├── .gitattributes ├── .gitignore ├── Benchmarks ├── Benchmarks.csproj ├── DeleteFromTextDocumentBufferBenchmark.cs ├── GetTextDocumentBufferBenchmark.cs ├── InsertInTextDocumentBufferBenchmark.cs └── Program.cs ├── CsharpPieceTableImplementation.sln ├── CsharpPieceTableImplementation ├── CsharpPieceTableImplementation.csproj ├── GlobalUsings.cs ├── Piece.cs ├── Span.cs ├── StringSpanPool.cs ├── TextDocumentBuffer.cs └── TextPieceTable.cs ├── LICENSE.md ├── README.md └── UnitTests ├── PieceTest.cs ├── TextDocumentBufferTest.cs └── UnitTests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | disable 8 | 9 | 10 | 11 | AnyCPU 12 | pdbonly 13 | true 14 | true 15 | true 16 | Release 17 | false 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Benchmarks/DeleteFromTextDocumentBufferBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CsharpPieceTableImplementation; 3 | 4 | namespace Benchmarks 5 | { 6 | [MemoryDiagnoser] 7 | [InProcess] 8 | public class DeleteFromTextDocumentBufferBenchmark 9 | { 10 | private TextDocumentBuffer _textDocumentBuffer; 11 | 12 | [Params(1_000, 10_000, 100_000, 1_000_000)] 13 | public int InitialNumberOfPiecesInPieceTable; 14 | 15 | [IterationSetup] 16 | public void IterationSetup() 17 | { 18 | _textDocumentBuffer = new TextDocumentBuffer(Array.Empty()); 19 | for (int i = 0; i < InitialNumberOfPiecesInPieceTable; i++) 20 | { 21 | _textDocumentBuffer.Insert(0, "Hello"); 22 | } 23 | } 24 | 25 | [Benchmark(Description = "Delete near the end of the document")] 26 | public void DeleteNearEndOfDocument() 27 | { 28 | _textDocumentBuffer.Delete(new Span(_textDocumentBuffer.DocumentLength - 3, 1)); 29 | } 30 | 31 | [Benchmark(Description = "Delete near the beginning of the document")] 32 | public void DeleteNearBeginningOfDocument() 33 | { 34 | _textDocumentBuffer.Delete(new Span(3, 1)); 35 | } 36 | 37 | [Benchmark(Description = "Delete in the middle of the document")] 38 | public void DeleteInMiddleOfDocument() 39 | { 40 | int middle = _textDocumentBuffer.DocumentLength / 2; 41 | _textDocumentBuffer.Delete(new Span(Math.Max(0, middle - 2), 4)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Benchmarks/GetTextDocumentBufferBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CsharpPieceTableImplementation; 3 | 4 | namespace Benchmarks 5 | { 6 | [MemoryDiagnoser] 7 | [InProcess] 8 | public class GetTextDocumentBufferBenchmark 9 | { 10 | private TextDocumentBuffer _textDocumentBuffer; 11 | 12 | [Params(1_000, 10_000, 100_000, 1_000_000)] 13 | public int InitialNumberOfPiecesInPieceTable; 14 | 15 | [IterationSetup] 16 | public void IterationSetup() 17 | { 18 | _textDocumentBuffer = new TextDocumentBuffer(Array.Empty()); 19 | for (int i = 0; i < InitialNumberOfPiecesInPieceTable; i++) 20 | { 21 | _textDocumentBuffer.Insert(0, "Hello"); 22 | } 23 | } 24 | 25 | [Benchmark(Description = "Get some text near the end of the document")] 26 | public void GetTextAtNearOfDocument() 27 | { 28 | _ = _textDocumentBuffer.GetText(new Span(_textDocumentBuffer.DocumentLength - 20, 9)); 29 | } 30 | 31 | [Benchmark(Description = "Get some text near the beginning of the document")] 32 | public void GetTextNearBeginningOfDocument() 33 | { 34 | _ = _textDocumentBuffer.GetText(new Span(20, 9)); 35 | } 36 | 37 | [Benchmark(Description = "Get some text in the middle of the document")] 38 | public void GetTextInMiddleOfDocument() 39 | { 40 | int middle = _textDocumentBuffer.DocumentLength / 2; 41 | _ = _textDocumentBuffer.GetText(new Span(middle - 3, 3)); 42 | } 43 | 44 | [Benchmark(Description = "Get a character near the end of the document")] 45 | public void GetCharacterAtEndOfDocument() 46 | { 47 | _ = _textDocumentBuffer[_textDocumentBuffer.DocumentLength - 20]; 48 | } 49 | 50 | [Benchmark(Description = "Get a character near the beginning of the document")] 51 | public void GetCharacterAtBeginningOfDocument() 52 | { 53 | _ = _textDocumentBuffer[20]; 54 | } 55 | 56 | [Benchmark(Description = "Get a character in the middle of the document")] 57 | public void GetCharacterInMiddleOfDocument() 58 | { 59 | int middle = _textDocumentBuffer.DocumentLength / 2; 60 | _ = _textDocumentBuffer[middle]; 61 | } 62 | 63 | [Benchmark(Description = "Get all the text in the document")] 64 | public void GetFullTextDocument() 65 | { 66 | _ = _textDocumentBuffer.GetText(new Span(0, _textDocumentBuffer.DocumentLength)); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Benchmarks/InsertInTextDocumentBufferBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CsharpPieceTableImplementation; 3 | 4 | namespace Benchmarks 5 | { 6 | [MemoryDiagnoser] 7 | [InProcess] 8 | public class InsertInTextDocumentBufferBenchmark 9 | { 10 | private TextDocumentBuffer _textDocumentBuffer; 11 | 12 | [Params(1_000, 10_000, 100_000, 1_000_000)] 13 | public int InitialNumberOfPiecesInPieceTable; 14 | 15 | [IterationSetup] 16 | public void IterationSetup() 17 | { 18 | _textDocumentBuffer 19 | = new TextDocumentBuffer(Array.Empty()); 20 | 21 | for (int i = 0; i < InitialNumberOfPiecesInPieceTable; i++) 22 | { 23 | _textDocumentBuffer.Insert(_textDocumentBuffer.DocumentLength, "Hello"); 24 | } 25 | } 26 | 27 | [Benchmark(Description = "Insertion near the end of the document")] 28 | public void InsertNearEndOfDocument() 29 | { 30 | _textDocumentBuffer.Insert(_textDocumentBuffer.DocumentLength - 3, 'A'); 31 | } 32 | 33 | [Benchmark(Description = "Insertion near the beginning of the document")] 34 | public void InsertNearBeginningOfDocument() 35 | { 36 | _textDocumentBuffer.Insert(3, 'A'); 37 | } 38 | 39 | [Benchmark(Description = "Insertion in the middle of the document")] 40 | public void InsertInMiddleOfDocument() 41 | { 42 | int middle = _textDocumentBuffer.DocumentLength / 2; 43 | _textDocumentBuffer.Insert(middle, 'A'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Benchmarks; 3 | 4 | _ = BenchmarkRunner.Run(); 5 | _ = BenchmarkRunner.Run(); 6 | _ = BenchmarkRunner.Run(); -------------------------------------------------------------------------------- /CsharpPieceTableImplementation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsharpPieceTableImplementation", "CsharpPieceTableImplementation\CsharpPieceTableImplementation.csproj", "{E0E61A33-FD8B-41A6-B9E1-D299D0FACAF4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{F6CAFC02-B6DB-4153-9179-5F269EE0DB03}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{583FA6FA-A732-441A-AD14-1FDD19A17A15}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {E0E61A33-FD8B-41A6-B9E1-D299D0FACAF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {E0E61A33-FD8B-41A6-B9E1-D299D0FACAF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {E0E61A33-FD8B-41A6-B9E1-D299D0FACAF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {E0E61A33-FD8B-41A6-B9E1-D299D0FACAF4}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {F6CAFC02-B6DB-4153-9179-5F269EE0DB03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F6CAFC02-B6DB-4153-9179-5F269EE0DB03}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F6CAFC02-B6DB-4153-9179-5F269EE0DB03}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F6CAFC02-B6DB-4153-9179-5F269EE0DB03}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {583FA6FA-A732-441A-AD14-1FDD19A17A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {583FA6FA-A732-441A-AD14-1FDD19A17A15}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {583FA6FA-A732-441A-AD14-1FDD19A17A15}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {583FA6FA-A732-441A-AD14-1FDD19A17A15}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {69A76DE2-D9DC-4199-9E23-64E7F0881581} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/CsharpPieceTableImplementation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0 5 | netstandard2.1 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using CommunityToolkit.Diagnostics; 2 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/Piece.cs: -------------------------------------------------------------------------------- 1 | namespace CsharpPieceTableImplementation 2 | { 3 | /// 4 | /// Represents the metadata for a piece. 5 | /// 6 | public record Piece : IEquatable 7 | { 8 | public Piece(bool isOriginal, int start, int length) 9 | : this (isOriginal, new Span(start, length)) 10 | { 11 | } 12 | 13 | public Piece(bool isOriginal, Span span) 14 | { 15 | Guard.IsGreaterThan(span.Length, 0); 16 | IsOriginal = isOriginal; 17 | Span = span; 18 | } 19 | 20 | /// 21 | /// Gets whether this is the `original` buffer or `add` buffer. 22 | /// 23 | public bool IsOriginal { get; } 24 | 25 | /// 26 | /// Gets the span this piece is in the appropriate buffer. 27 | /// 28 | public Span Span { get; } 29 | 30 | /// 31 | /// Determines whether two pieces are the same. 32 | /// 33 | /// The piece to compare. 34 | public virtual bool Equals(Piece? other) 35 | { 36 | if (ReferenceEquals(this, other)) 37 | { 38 | return true; 39 | } 40 | 41 | return other is not null && other.Span == Span && other.IsOriginal == IsOriginal; 42 | } 43 | 44 | /// 45 | /// Provides a hash function for the type. 46 | /// 47 | public override int GetHashCode() 48 | { 49 | return Span.GetHashCode() + IsOriginal.GetHashCode(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/Span.cs: -------------------------------------------------------------------------------- 1 | // FROM: https://github.com/microsoft/vs-editor-api, under MIT license. 2 | 3 | namespace CsharpPieceTableImplementation 4 | { 5 | /// 6 | /// An immutable integer interval that describes a range of values from to that is closed on 7 | /// the left and open on the right: [Start .. End). A zpan is usually applied to an to denote a span of text, 8 | /// but it is independent of any particular text buffer or snapshot. 9 | /// 10 | public readonly struct Span 11 | { 12 | private readonly int _start; 13 | private readonly int _length; 14 | 15 | /// 16 | /// Initializes a new instance of a with the given start point and length. 17 | /// 18 | /// 19 | /// The starting point of the span. 20 | /// 21 | /// 22 | /// The length of the span. 23 | /// 24 | /// or is less than zero, or 25 | /// start + length is greater than the length of the text snapshot. 26 | public Span(int start, int length) 27 | { 28 | Guard.IsGreaterThanOrEqualTo(start, 0); 29 | Guard.IsGreaterThanOrEqualTo(start + length, start); 30 | _start = start; 31 | _length = length; 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of a with the given start and end positions. 36 | /// 37 | /// The start position of the new span. 38 | /// The end position of the new Span. 39 | /// The new span. 40 | /// is less than zero, or 41 | /// is less than . 42 | public static Span FromBounds(int start, int end) 43 | { 44 | // We don't need to check arguments, as the Span constructor will check for us. 45 | return new Span(start, end - start); 46 | } 47 | 48 | /// 49 | /// The starting index of the span. 50 | /// 51 | public int Start => _start; 52 | 53 | /// 54 | /// The end of the span. The span is open-ended on the right side, which is to say 55 | /// that Start + Length = End. 56 | /// 57 | public int End => _start + _length; 58 | 59 | /// 60 | /// The length of the span, which is always non-negative. 61 | /// 62 | public int Length => _length; 63 | 64 | /// 65 | /// Determines whether or not this span is empty. 66 | /// 67 | /// true if the length of the span is zero, otherwise false. 68 | public bool IsEmpty => _length == 0; 69 | 70 | /// 71 | /// Determines whether the position lies within the span. 72 | /// 73 | /// 74 | /// The position to check. 75 | /// 76 | /// 77 | /// true if the position is greater than or equal to Start and strictly less 78 | /// than End, otherwise false. 79 | /// 80 | public bool Contains(int position) 81 | { 82 | return position >= _start && position < End; 83 | } 84 | 85 | /// 86 | /// Determines whether falls completely within this span. 87 | /// 88 | /// 89 | /// The span to check. 90 | /// 91 | /// 92 | /// true if the specified span falls completely within this span, otherwise false. 93 | /// 94 | public bool Contains(Span span) 95 | { 96 | return span.Start >= _start && span.End <= End; 97 | } 98 | 99 | /// 100 | /// Determines whether overlaps this span. Two spans are considered to overlap 101 | /// if they have positions in common and neither is empty. Empty spans do not overlap with any 102 | /// other span. 103 | /// 104 | /// 105 | /// The span to check. 106 | /// 107 | /// 108 | /// true if the spans overlap, otherwise false. 109 | /// 110 | public bool OverlapsWith(Span span) 111 | { 112 | int overlapStart = Math.Max(_start, span.Start); 113 | int overlapEnd = Math.Min(End, span.End); 114 | 115 | return overlapStart < overlapEnd; 116 | } 117 | 118 | /// 119 | /// Returns the overlap with the given span, or null if there is no overlap. 120 | /// 121 | /// 122 | /// The span to check. 123 | /// 124 | /// 125 | /// The overlap of the spans, or null if the overlap is empty. 126 | /// 127 | public Span? Overlap(Span span) 128 | { 129 | int overlapStart = Math.Max(_start, span.Start); 130 | int overlapEnd = Math.Min(End, span.End); 131 | 132 | if (overlapStart < overlapEnd) 133 | { 134 | return FromBounds(overlapStart, overlapEnd); 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /// 141 | /// Determines whether intersects this span. Two spans are considered to 142 | /// intersect if they have positions in common or the end of one span 143 | /// coincides with the start of the other span. 144 | /// 145 | /// 146 | /// The span to check. 147 | /// 148 | /// 149 | /// true if the spans intersect, otherwise false. 150 | /// 151 | public bool IntersectsWith(Span span) 152 | { 153 | return span.Start <= End && span.End >= _start; 154 | } 155 | 156 | /// 157 | /// Returns the intersection with the given span, or null if there is no intersection. 158 | /// 159 | /// 160 | /// The span to check. 161 | /// 162 | /// 163 | /// The intersection of the spans, or null if the intersection is empty. 164 | /// 165 | public Span? Intersection(Span span) 166 | { 167 | int intersectStart = Math.Max(_start, span.Start); 168 | int intersectEnd = Math.Min(End, span.End); 169 | 170 | if (intersectStart <= intersectEnd) 171 | { 172 | return FromBounds(intersectStart, intersectEnd); 173 | } 174 | 175 | return null; 176 | } 177 | 178 | /// 179 | /// Provides a string representation of the span. 180 | /// 181 | public override string ToString() 182 | { 183 | return string.Format( 184 | System.Globalization.CultureInfo.InvariantCulture, 185 | "[{0}..{1})", 186 | _start, 187 | _start + _length); 188 | } 189 | 190 | /// 191 | /// Provides a hash function for the type. 192 | /// 193 | public override int GetHashCode() 194 | { 195 | return Length.GetHashCode() ^ Start.GetHashCode(); 196 | } 197 | 198 | /// 199 | /// Determines whether two spans are the same. 200 | /// 201 | /// The object to compare. 202 | public override bool Equals(object? obj) 203 | { 204 | if (obj is Span other) 205 | { 206 | return other.Start == _start && other.Length == _length; 207 | } 208 | else 209 | { 210 | return false; 211 | } 212 | } 213 | 214 | /// 215 | /// Determines whether two spans are the same 216 | /// 217 | public static bool operator ==(Span left, Span right) 218 | { 219 | return left.Start == right.Start && left.Length == right.Length; 220 | } 221 | 222 | /// 223 | /// Determines whether two spans are different. 224 | /// 225 | public static bool operator !=(Span left, Span right) 226 | { 227 | return !(left == right); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/StringSpanPool.cs: -------------------------------------------------------------------------------- 1 | namespace CsharpPieceTableImplementation 2 | { 3 | public sealed class StringSpanPool 4 | { 5 | private readonly Dictionary _cache = new(); 6 | 7 | public string? GetStringFromCache(Span span) 8 | { 9 | if (_cache.TryGetValue(span, out string? result)) 10 | { 11 | return result; 12 | } 13 | 14 | return null; 15 | } 16 | 17 | public void Cache(Span span, string entry) 18 | { 19 | _cache.TryAdd(span, entry); 20 | } 21 | 22 | public void Reset() 23 | { 24 | _cache.Clear(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/TextDocumentBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace CsharpPieceTableImplementation 4 | { 5 | /// 6 | /// Represents a buffer that contains the original text of the document and all the changes made by the user. 7 | /// This is an public implementation of the PieceTable data structure. 8 | /// 9 | public sealed class TextDocumentBuffer 10 | { 11 | private readonly ReadOnlyMemory _originalDocumentBuffer; 12 | private readonly List _appendBuffer = new(); // max size is 2GB (approximately 1 billion characters) 13 | private readonly TextPieceTable _pieceTable; 14 | private readonly StringSpanPool _stringSpanPool = new(); 15 | 16 | public TextDocumentBuffer(char[] originalDocument) 17 | { 18 | Guard.IsNotNull(originalDocument, nameof(originalDocument)); 19 | 20 | _originalDocumentBuffer = originalDocument; 21 | _pieceTable = new TextPieceTable(originalDocument.Length); 22 | } 23 | 24 | /// 25 | /// Gets the character at the given text document position. 26 | /// 27 | public char this[int textDocumentPosition] 28 | { 29 | get 30 | { 31 | _pieceTable.FindPieceFromTextDocumentPosition(textDocumentPosition, out Piece piece, out int pieceStartPositionInDocument); 32 | 33 | int bufferPosition = piece.Span.Start + (textDocumentPosition - pieceStartPositionInDocument); 34 | 35 | if (piece.IsOriginal) 36 | { 37 | return _originalDocumentBuffer.Span[bufferPosition]; 38 | } 39 | else 40 | { 41 | return _appendBuffer[bufferPosition]; 42 | } 43 | } 44 | } 45 | 46 | /// 47 | /// Gets the current length of the document. 48 | /// 49 | public int DocumentLength => _pieceTable.DocumentLength; 50 | 51 | /// 52 | /// Get the text that corresponds to the given . 53 | /// 54 | /// 55 | /// This method can allocate a lot of memory because it rebuilds a string from the piece table. Use it caution. 56 | /// 57 | public string GetText(Span spanInTextDocument) 58 | { 59 | if (spanInTextDocument.IsEmpty) 60 | { 61 | return string.Empty; 62 | } 63 | 64 | // Check whether this span has already been asked in the past. If yes, we already 65 | // have a string for it, no need to instantiate a new one. 66 | string? result = _stringSpanPool.GetStringFromCache(spanInTextDocument); 67 | if (!string.IsNullOrEmpty(result)) 68 | { 69 | return result; 70 | } 71 | 72 | // Let's find all the pieces that overlap the given text document span. 73 | _pieceTable.FindPiecesCoveringTextDocumentSpan(spanInTextDocument, out IReadOnlyList pieces, out int pieceStartPositionInDocument); 74 | 75 | var builder = new StringBuilder(); 76 | 77 | for (int i = 0; i < pieces.Count; i++) 78 | { 79 | Piece piece = pieces[i]; 80 | 81 | // By default, we retrieve the full piece's span from the buffer. 82 | int bufferPositionStart = piece.Span.Start; 83 | int bufferLength = piece.Span.Length; 84 | 85 | // But if we're on the first or last piece, it's possible that the piece start and end may not corresponds 86 | // to spanInTextDocument's boundaries. We need to adjust the start and length of what to grab from the piece. 87 | if (i == 0) 88 | { 89 | bufferPositionStart = piece.Span.Start + (spanInTextDocument.Start - pieceStartPositionInDocument); 90 | bufferLength = piece.Span.Start + piece.Span.Length - bufferPositionStart; 91 | } 92 | 93 | pieceStartPositionInDocument += piece.Span.Length; 94 | 95 | if (i == pieces.Count - 1 && pieceStartPositionInDocument > spanInTextDocument.End) 96 | { 97 | int bufferPositionEnd = piece.Span.End - (pieceStartPositionInDocument - spanInTextDocument.End); 98 | bufferLength = bufferPositionEnd - bufferPositionStart; 99 | } 100 | 101 | // Pick up the characters from the right buffer. 102 | if (piece.IsOriginal) 103 | { 104 | builder.Append(_originalDocumentBuffer.Span.Slice(bufferPositionStart, bufferLength)); 105 | } 106 | else 107 | { 108 | for (int j = bufferPositionStart; j < bufferPositionStart + bufferLength; j++) 109 | { 110 | builder.Append(_appendBuffer[j]); 111 | } 112 | } 113 | } 114 | 115 | // Generate the final string. 116 | result = builder.ToString()!; 117 | 118 | // Cache it, so we don't have to instantiate it again if we ask multiple time the same span. 119 | _stringSpanPool.Cache(spanInTextDocument, result); 120 | 121 | return result; 122 | } 123 | 124 | /// 125 | /// Inserts a character at a given position in the text document. 126 | /// 127 | /// 128 | /// The character will be added to the append buffer. 129 | /// 130 | public void Insert(int textDocumentPosition, char @char) 131 | { 132 | // TODO: Potential optimization: 133 | // It's likely possible that inserted characters to a text document are very redundant. For example, 134 | // a descriptive text in English likely use alphnumeric characters (a-zA-Z0-9). Therefore, to economize 135 | // some memory, we could lookup in the _appendBuffer whether the @char already exist, and if yes, 136 | // simply pass its location as a Span and not add the character to the buffer. 137 | // 138 | // This optimization is potentially possible in the method using string instead of char too, but is likely 139 | // more expensive for less improvement. 140 | // 141 | // A (potential) drawback of this optimization is that several spans may have the same start and length, which 142 | // could potentially open a door to maintainability madness and bugs. 143 | // 144 | // Overall, this optimization would be benifical when the user types a character after another in the document. 145 | 146 | if (textDocumentPosition != DocumentLength) 147 | { 148 | // Reset the cache of string, since the insert will compromized it. 149 | _stringSpanPool.Reset(); 150 | } 151 | 152 | _pieceTable.Insert(textDocumentPosition, new Span(_appendBuffer.Count, 1)); 153 | _appendBuffer.Add(@char); 154 | } 155 | 156 | /// 157 | /// Inserts a string at a given position in the text document. 158 | /// 159 | /// 160 | /// The text will be added to the append buffer. 161 | /// 162 | public void Insert(int textDocumentPosition, string text) 163 | { 164 | if (string.IsNullOrEmpty(text)) 165 | { 166 | return; 167 | } 168 | 169 | if (textDocumentPosition != DocumentLength) 170 | { 171 | // Reset the cache of string, since the insert will compromized it. 172 | _stringSpanPool.Reset(); 173 | } 174 | 175 | _pieceTable.Insert(textDocumentPosition, new Span(_appendBuffer.Count, text.Length)); 176 | _appendBuffer.AddRange(text); 177 | } 178 | 179 | /// 180 | /// Deletes the given span from the text document. 181 | /// 182 | /// 183 | /// This does not remove the text from the buffer. With that in mind, some scenarios like Undo/Redo can be designed 184 | /// on top of the piece table. 185 | /// 186 | public void Delete(Span textDocumentSpan) 187 | { 188 | _pieceTable.Delete(textDocumentSpan); 189 | 190 | // Reset the cache of string, since the delete will compromized it. 191 | _stringSpanPool.Reset(); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /CsharpPieceTableImplementation/TextPieceTable.cs: -------------------------------------------------------------------------------- 1 | namespace CsharpPieceTableImplementation 2 | { 3 | /// 4 | /// Provides a representation of the current text document by keeping track of what pieces of 5 | /// the original and append buffer should be used to build the document. 6 | /// The text document may be up to 2GB in size (approximately 1 billion characters), which is 7 | /// the maximum limit of the C# String data type. 8 | /// 9 | public sealed class TextPieceTable 10 | { 11 | private readonly LinkedList _pieces = new(); 12 | 13 | public TextPieceTable(int originalDocumentLength) 14 | { 15 | Guard.IsGreaterThanOrEqualTo(originalDocumentLength, 0); 16 | 17 | if (originalDocumentLength > 0) 18 | { 19 | _pieces.AddLast(new Piece(isOriginal: true, 0, originalDocumentLength)); 20 | } 21 | 22 | DocumentLength = originalDocumentLength; 23 | } 24 | 25 | /// 26 | /// Gets the length of the text document. 27 | /// 28 | public int DocumentLength { get; private set; } 29 | 30 | /// 31 | /// Inserts into the piece table. 32 | /// 33 | /// The position in the text document where the piece will correspond to. 34 | /// A span representing the part of the `append` buffer to insert at the given position in the text document. 35 | public void Insert(int textDocumentPosition, Span pieceTableBufferSpan) 36 | { 37 | if (pieceTableBufferSpan.IsEmpty) 38 | { 39 | return; 40 | } 41 | 42 | Guard.IsGreaterThanOrEqualTo(textDocumentPosition, 0); 43 | 44 | var pieceToInsert 45 | = new Piece( 46 | isOriginal: false, 47 | pieceTableBufferSpan); 48 | 49 | if (textDocumentPosition == DocumentLength) 50 | { 51 | // Fast path. We're adding something at the end of the document. Simply add the piece at the very end of the table. 52 | _pieces.AddLast(pieceToInsert); 53 | } 54 | else if (textDocumentPosition == 0) 55 | { 56 | // Fast path. We're adding something at the very beginning of the document. 57 | _pieces.AddFirst(pieceToInsert); 58 | } 59 | else 60 | { 61 | // We're adding in the middle of the document. 62 | // Let's find what piece is at the insertion position. 63 | FindPieceFromTextDocumentPosition( 64 | textDocumentPosition, 65 | out LinkedListNode existingPieceAtPositionInTextDocument, 66 | out int pieceStartPositionInDocument); 67 | 68 | if (textDocumentPosition == pieceStartPositionInDocument) 69 | { 70 | // If we are at the start boundary of the existing piece, we can simply insert the new piece at the beginning of it. 71 | _pieces.AddBefore(existingPieceAtPositionInTextDocument, pieceToInsert); 72 | } 73 | else if (textDocumentPosition == pieceStartPositionInDocument + existingPieceAtPositionInTextDocument.Value.Span.Length) 74 | { 75 | // If we are the end boundary, we can simply insert after the existing piece. 76 | _pieces.AddAfter(existingPieceAtPositionInTextDocument, pieceToInsert); 77 | } 78 | else 79 | { 80 | // The insertion position is in the middle of an existing piece. Therefore, we need to split this piece 81 | // in 3: 82 | // 1. A piece that represents the text that is before the inserted piece. 83 | // 2. The insertion piece itself. 84 | // 3. A piece that represents the text that is after the inserted piece. 85 | 86 | // Find the position at which to split, relative to the start of the current piece. 87 | int insertionPositionInExistingPiece = textDocumentPosition - pieceStartPositionInDocument; 88 | 89 | // Calculate length of the piece before and after the insertion. 90 | int beforeInsertionPieceLength = insertionPositionInExistingPiece; 91 | int afterInsertionPieceLength = existingPieceAtPositionInTextDocument.Value.Span.Length - beforeInsertionPieceLength; 92 | 93 | // Create the pieces. 94 | var beforeInsertionPiece 95 | = new Piece( 96 | existingPieceAtPositionInTextDocument.Value.IsOriginal, 97 | existingPieceAtPositionInTextDocument.Value.Span.Start, 98 | beforeInsertionPieceLength); 99 | 100 | var afterInsertionPiece 101 | = new Piece( 102 | existingPieceAtPositionInTextDocument.Value.IsOriginal, 103 | insertionPositionInExistingPiece + existingPieceAtPositionInTextDocument.Value.Span.Start, 104 | afterInsertionPieceLength); 105 | 106 | // Insert we have three pieces: |piece before insertion| |insertion| |piece after insertion| 107 | existingPieceAtPositionInTextDocument.Value = beforeInsertionPiece; 108 | LinkedListNode insertedPieceNode = _pieces.AddAfter(existingPieceAtPositionInTextDocument, pieceToInsert); 109 | _pieces.AddAfter(insertedPieceNode, afterInsertionPiece); 110 | } 111 | } 112 | 113 | // Update the document lenght. 114 | DocumentLength += pieceToInsert.Span.Length; 115 | } 116 | 117 | /// 118 | /// Delete from the piece table. 119 | /// 120 | /// A span representing a part of the text document to remove. 121 | public void Delete(Span textDocumentSpan) 122 | { 123 | if (textDocumentSpan.IsEmpty) 124 | { 125 | return; 126 | } 127 | 128 | // Let's find all the pieces that overlap the given text document span. 129 | FindPiecesCoveringTextDocumentSpan( 130 | textDocumentSpan, 131 | out IReadOnlyList> existingPiecesNodeCoveringSpanInTextDocument, 132 | out _, 133 | out int startPositionInTextDocumentOfFirstPiece); 134 | 135 | if (existingPiecesNodeCoveringSpanInTextDocument.Count == 1) 136 | { 137 | // The span to delete is within the boundaries of a single piece in the table. Let's use a faster path here. 138 | LinkedListNode existingPieceAtStartPositionInTextDocument = existingPiecesNodeCoveringSpanInTextDocument[0]; 139 | DeleteSpanFittingBoundariesOfASinglePiece(textDocumentSpan, existingPieceAtStartPositionInTextDocument, startPositionInTextDocumentOfFirstPiece); 140 | } 141 | else 142 | { 143 | // The span to delete overlaps many pieces in the table. 144 | DeleteSpanOverlapingManyPieces(textDocumentSpan, existingPiecesNodeCoveringSpanInTextDocument, startPositionInTextDocumentOfFirstPiece); 145 | } 146 | 147 | // Update the document lenght. 148 | DocumentLength -= textDocumentSpan.Length; 149 | } 150 | 151 | /// 152 | /// Finds the piece corresponding to the given position in the text document. 153 | /// 154 | /// The position in the text document where the piece will correspond to. 155 | /// The piece that contains the . 156 | /// The position in the text document where the starts. 157 | public void FindPieceFromTextDocumentPosition(int textDocumentPosition, out Piece piece, out int startPositionInTextDocumentOfPiece) 158 | { 159 | FindPieceFromTextDocumentPosition(textDocumentPosition, out LinkedListNode pieceNode, out startPositionInTextDocumentOfPiece); 160 | piece = pieceNode.Value; 161 | } 162 | 163 | /// 164 | /// Finds all the pieces that cover the given span in the text document. 165 | /// 166 | /// A span representing a part of the text document. 167 | /// The list of pieces that overlap the . 168 | /// The position in the text document where the first starts. 169 | public void FindPiecesCoveringTextDocumentSpan(Span textDocumentSpan, out IReadOnlyList pieces, out int startPositionInTextDocumentOfFirstPiece) 170 | { 171 | FindPiecesCoveringTextDocumentSpan(textDocumentSpan, out _, out pieces, out startPositionInTextDocumentOfFirstPiece); 172 | } 173 | 174 | private void FindPieceFromTextDocumentPosition(int textDocumentPosition, out LinkedListNode pieceNode, out int startPositionInTextDocumentOfPiece) 175 | { 176 | Guard.IsGreaterThanOrEqualTo(textDocumentPosition, 0); 177 | 178 | // If the text document position is in the second half of the document, we get better chance to find the piece 179 | // faster by search backward in the linked list. 180 | bool searchFromTheEnd = textDocumentPosition > DocumentLength / 2; 181 | 182 | if (searchFromTheEnd) 183 | { 184 | // Search backward. 185 | SearchBackwardPieceFromTextDocumentPosition(textDocumentPosition, out pieceNode, out startPositionInTextDocumentOfPiece); 186 | } 187 | else 188 | { 189 | // Search forward. 190 | SearchForwardPieceFromTextDocumentPosition(textDocumentPosition, out pieceNode, out startPositionInTextDocumentOfPiece); 191 | } 192 | } 193 | 194 | private void SearchForwardPieceFromTextDocumentPosition(int textDocumentPosition, out LinkedListNode pieceNode, out int startPositionInTextDocumentOfPiece) 195 | { 196 | int pieceEndPositionInDocument = 0; 197 | int pieceStartPositionInDocument = 0; 198 | int i = 0; 199 | LinkedListNode? node = _pieces.First; 200 | while (i < _pieces.Count && node is not null) 201 | { 202 | pieceEndPositionInDocument += node.Value.Span.Length; 203 | if (pieceEndPositionInDocument > textDocumentPosition) 204 | { 205 | pieceNode = node; 206 | startPositionInTextDocumentOfPiece = pieceStartPositionInDocument; 207 | return; 208 | } 209 | 210 | pieceStartPositionInDocument = pieceEndPositionInDocument; 211 | 212 | node = node.Next; 213 | i++; 214 | } 215 | 216 | throw new IndexOutOfRangeException("The given position is greater than the text document length."); 217 | } 218 | 219 | private void SearchBackwardPieceFromTextDocumentPosition(int textDocumentPosition, out LinkedListNode pieceNode, out int startPositionInTextDocumentOfPiece) 220 | { 221 | int pieceStartPositionInDocument = DocumentLength; 222 | int i = _pieces.Count; 223 | LinkedListNode? node = _pieces.Last; 224 | while (i >= 0 && node is not null) 225 | { 226 | pieceStartPositionInDocument -= node.Value.Span.Length; 227 | if (pieceStartPositionInDocument <= textDocumentPosition) 228 | { 229 | pieceNode = node; 230 | startPositionInTextDocumentOfPiece = pieceStartPositionInDocument; 231 | return; 232 | } 233 | 234 | node = node.Previous; 235 | i--; 236 | } 237 | 238 | throw new IndexOutOfRangeException("The given position is greater than the text document length."); 239 | } 240 | 241 | private void FindPiecesCoveringTextDocumentSpan(Span textDocumentSpan, out IReadOnlyList> pieceNodes, out IReadOnlyList pieces, out int startPositionInTextDocumentOfFirstPiece) 242 | { 243 | if (textDocumentSpan.IsEmpty) 244 | { 245 | pieceNodes = Array.Empty>(); 246 | pieces = Array.Empty(); 247 | startPositionInTextDocumentOfFirstPiece = 0; 248 | return; 249 | } 250 | 251 | // If the text document span's start position is in the second half of the document, we get better chance to find the piece 252 | // faster by search backward in the linked list. 253 | bool searchFromTheEnd = textDocumentSpan.Start > DocumentLength / 2; 254 | 255 | if (searchFromTheEnd) 256 | { 257 | // Search backward. 258 | SearchBackwardPiecesCoveringTextDocumentSpan(textDocumentSpan, out pieceNodes, out pieces, out startPositionInTextDocumentOfFirstPiece); 259 | } 260 | else 261 | { 262 | // Search forward. 263 | SearchForwardPiecesCoveringTextDocumentSpan(textDocumentSpan, out pieceNodes, out pieces, out startPositionInTextDocumentOfFirstPiece); 264 | } 265 | } 266 | 267 | private void SearchForwardPiecesCoveringTextDocumentSpan(Span textDocumentSpan, out IReadOnlyList> pieceNodes, out IReadOnlyList pieces, out int startPositionInTextDocumentOfFirstPiece) 268 | { 269 | startPositionInTextDocumentOfFirstPiece = -1; 270 | var resultedPieceNodes = new List>(); 271 | var resultsPieces = new List(); 272 | int characterCount = 0; 273 | int characterBeforeNextPieceCount = 0; 274 | 275 | int i = 0; 276 | LinkedListNode? node = _pieces.First; 277 | while (i < _pieces.Count && node is not null) 278 | { 279 | characterCount += node.Value.Span.Length; 280 | if (characterCount > textDocumentSpan.Start) 281 | { 282 | if (resultedPieceNodes.Count == 0) 283 | { 284 | startPositionInTextDocumentOfFirstPiece = characterBeforeNextPieceCount; 285 | } 286 | 287 | resultedPieceNodes.Add(node); 288 | resultsPieces.Add(node.Value); 289 | 290 | if (characterCount >= textDocumentSpan.End) 291 | { 292 | pieceNodes = resultedPieceNodes; 293 | pieces = resultsPieces; 294 | Guard.IsGreaterThan(startPositionInTextDocumentOfFirstPiece, -1); 295 | return; 296 | } 297 | } 298 | 299 | characterBeforeNextPieceCount = characterCount; 300 | 301 | node = node.Next; 302 | i++; 303 | } 304 | 305 | throw new IndexOutOfRangeException("The span end position is greated than the text document length."); 306 | } 307 | 308 | private void SearchBackwardPiecesCoveringTextDocumentSpan(Span textDocumentSpan, out IReadOnlyList> pieceNodes, out IReadOnlyList pieces, out int startPositionInTextDocumentOfFirstPiece) 309 | { 310 | startPositionInTextDocumentOfFirstPiece = -1; 311 | var resultedPieceNodes = new List>(); 312 | var resultsPieces = new List(); 313 | int pieceCountToRead = 0; 314 | int pieceStartPositionInDocument = DocumentLength; 315 | 316 | int i = _pieces.Count; 317 | LinkedListNode? node = _pieces.Last; 318 | 319 | while (i >= 0 && node is not null) 320 | { 321 | pieceStartPositionInDocument -= node.Value.Span.Length; 322 | if (pieceStartPositionInDocument <= textDocumentSpan.End && resultedPieceNodes.Count == 0) 323 | { 324 | pieceCountToRead++; 325 | } 326 | 327 | if (pieceStartPositionInDocument <= textDocumentSpan.Start) 328 | { 329 | Guard.IsNotNull(node); 330 | LinkedListNode? currentPieceToAddToResult = node; 331 | int j = 0; 332 | 333 | while (j < pieceCountToRead && currentPieceToAddToResult is not null) 334 | { 335 | resultedPieceNodes.Add(currentPieceToAddToResult); 336 | resultsPieces.Add(currentPieceToAddToResult.Value); 337 | currentPieceToAddToResult = currentPieceToAddToResult.Next; 338 | j++; 339 | } 340 | 341 | startPositionInTextDocumentOfFirstPiece = pieceStartPositionInDocument; 342 | pieceNodes = resultedPieceNodes; 343 | pieces = resultsPieces; 344 | Guard.IsGreaterThan(startPositionInTextDocumentOfFirstPiece, -1); 345 | return; 346 | } 347 | 348 | if (resultedPieceNodes.Count > 0) 349 | { 350 | pieceCountToRead++; 351 | } 352 | 353 | node = node.Previous; 354 | i--; 355 | } 356 | 357 | throw new IndexOutOfRangeException("The span end position is greated than the text document length."); 358 | } 359 | 360 | private void DeleteSpanFittingBoundariesOfASinglePiece(Span textDocumentSpan, LinkedListNode pieceNodeToCut, int startPositionInTextDocumentOfPieceToCut) 361 | { 362 | int pieceToTextDocumentSpanOffset = textDocumentSpan.Start - startPositionInTextDocumentOfPieceToCut; 363 | if (pieceToTextDocumentSpanOffset == 0) 364 | { 365 | // Simple case. We're deleting the beginning of the piece. Let's just resize the piece by trimming the 366 | // span at the beginning. 367 | int newLength = pieceNodeToCut.Value.Span.Length - textDocumentSpan.Length; 368 | if (newLength != 0) 369 | { 370 | int newStartPosition = pieceNodeToCut.Value.Span.End - newLength; 371 | pieceNodeToCut.Value 372 | = new Piece( 373 | pieceNodeToCut.Value.IsOriginal, 374 | newStartPosition, 375 | newLength); 376 | } 377 | else 378 | { 379 | // In fact, it looks like textDocumentSpan's length covers the entire pieceNodeToCut. 380 | // Therefore, we can just remove the piece since we don't want to keep a piece with an empty span (length == 0). 381 | _pieces.Remove(pieceNodeToCut); 382 | } 383 | } 384 | else if (textDocumentSpan.End == startPositionInTextDocumentOfPieceToCut + pieceNodeToCut.Value.Span.Length) 385 | { 386 | // Simple case too. We're deleting the end of the piece. Let's just resize the piece by trimming the 387 | // span at the end. 388 | int newLength = pieceNodeToCut.Value.Span.Length - textDocumentSpan.Length; 389 | if (newLength != 0) 390 | { 391 | pieceNodeToCut.Value 392 | = new Piece( 393 | pieceNodeToCut.Value.IsOriginal, 394 | pieceNodeToCut.Value.Span.Start, 395 | newLength); 396 | } 397 | else 398 | { 399 | // In fact, the entire piece is being removed. 400 | _pieces.Remove(pieceNodeToCut); 401 | } 402 | } 403 | else 404 | { 405 | // We're removing text somewhere in the middle of the piece. 406 | // Therefore, let's split the piece in 2: 407 | // 1. A piece that represents what's before the removed span. 408 | // 2. A piece that represents what's after the removed span. 409 | 410 | // Let's resizing the original piece to something smaller 411 | // that only represents what was before removed span. 412 | int resizeLength = pieceToTextDocumentSpanOffset; 413 | Piece backupExistingPiece = pieceNodeToCut.Value; 414 | pieceNodeToCut.Value 415 | = new Piece( 416 | pieceNodeToCut.Value.IsOriginal, 417 | pieceNodeToCut.Value.Span.Start, 418 | resizeLength); 419 | 420 | // Now, let's insert a new piece that represents what's after the removed span. 421 | int newLength = backupExistingPiece.Span.Length - textDocumentSpan.Length - pieceToTextDocumentSpanOffset; 422 | if (newLength != 0) 423 | { 424 | int newStartPosition = backupExistingPiece.Span.End - newLength; 425 | _pieces.AddAfter( 426 | pieceNodeToCut, 427 | new Piece( 428 | backupExistingPiece.IsOriginal, 429 | newStartPosition, 430 | newLength)); 431 | } 432 | } 433 | } 434 | 435 | private void DeleteSpanOverlapingManyPieces(Span textDocumentSpan, IReadOnlyList> piecesToCut, int startPositionInTextDocumentOfFirstPiece) 436 | { 437 | Guard.HasSizeGreaterThan(piecesToCut, 1); 438 | 439 | LinkedListNode firstPiece = piecesToCut[0]; 440 | LinkedListNode lastPiece = piecesToCut[piecesToCut.Count - 1]; 441 | int startPositionInTextDocumentOfLastPiece = startPositionInTextDocumentOfFirstPiece + firstPiece.Value.Span.Length; 442 | 443 | if (piecesToCut.Count > 2) 444 | { 445 | // Fast path. If there are 3 or more pieces to remove, let's delete all the pieces between the first and last one 446 | // since we know we have to remove them entirely. 447 | for (int i = 1; i < piecesToCut.Count - 1; i++) 448 | { 449 | LinkedListNode piece = piecesToCut[i]; 450 | startPositionInTextDocumentOfLastPiece += piece.Value.Span.Length; 451 | _pieces.Remove(piece); 452 | } 453 | } 454 | 455 | // The first and last piece may need to be splitted because the text document span may not fit perfectly the boundaries of these pieces. 456 | DeleteSpanFittingBoundariesOfASinglePiece( 457 | new Span( 458 | textDocumentSpan.Start, 459 | startPositionInTextDocumentOfFirstPiece + firstPiece.Value.Span.Length - textDocumentSpan.Start), 460 | firstPiece, 461 | startPositionInTextDocumentOfFirstPiece); 462 | 463 | DeleteSpanFittingBoundariesOfASinglePiece( 464 | new Span( 465 | startPositionInTextDocumentOfLastPiece, 466 | textDocumentSpan.End - startPositionInTextDocumentOfLastPiece), 467 | lastPiece, 468 | startPositionInTextDocumentOfLastPiece); 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Piece Table implementation in C# 2 | 3 | ## What is the Piece Table? 4 | 5 | Piece Table isn't a very common data structure and therefore isn't well documented. 6 | 7 | Here are a few helpful article that inspired me: 8 | 9 | * [The Piece Table, by Darren Burns](https://darrenburns.net/posts/piece-table/) 10 | 11 | * [Text Buffer Reimplementation, a Visual Studio Code Story](https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation#) 12 | 13 | * [Piece table - Wikipedia](https://en.wikipedia.org/wiki/Piece_table) 14 | 15 | ### TL;DR 16 | 17 | from Wikipedia: 18 | 19 | > A **piece table** is a data structure typically used to represent a series of edits on a text document. An initial reference (or 'span') to the whole of the original file is created, with subsequent inserts and deletes being created as combinations of one, two, or three references to sections of either the original document or of the spans associated with earlier inserts. 20 | > 21 | > Typically the text of the original document is held in one immutable block, and the text of each subsequent insert is stored in new immutable blocks. Because even deleted text is still included in the piece table, this makes multi-level or unlimited undo easier to implement with a piece table than with alternative data structures such as a gap buffer. 22 | 23 | ## Motivation of this repository 24 | 25 | I was curious about the implementation details of such a data structure, since I myself work on the text editor of Visual Studio at Microsoft. However, existing implementation available on GitHub, [including the one of Visual Studio itself](https://github.com/microsoft/vs-editor-api/tree/main/src/Editor/Text/Impl/TextModel/StringRebuilder), are either: 26 | 27 | 1. Complex to read because tightly linked to a product like VS or VS Code, and not generic enough to be reused as is. 28 | 29 | 2. Incomplete and/or buggy. 30 | 31 | 3. Not documented / commented. 32 | 33 | 4. In functional or procedural language (nothing against it but I like oriented object programming). 34 | 35 | So I thought `let's have fun and do it myself, hoping to have something reliable, generic and easy to understand`. 36 | 37 | ## Implementation 38 | 39 | If you take 10 minutes to read [The Piece Table, by Darren Burns](https://darrenburns.net/posts/piece-table/) and understand well how the Piece Table is supposed to work, then reading the code in this repository should be relatively easy as it reuses some terms you can find in this blog article. 40 | 41 | * `TextDocumentBuffer` hold the buffers that allow to represent and rebuild the text as a `string` after an insertion / deletion in the text document. 42 | 43 | * `TextPieceTable` hosts the `piece table` itself and the logic for inserting / deleting a piece. It has a bunch of logic for translating coordinates from the text document to a piece in the buffer. I uses a `LinkedList` for representing the list of pieces. 44 | 45 | * `Piece` represents a piece in the piece table. It has the following information: 46 | 47 | * Whether this piece is from the original or append buffer. 48 | 49 | * A `Span` representing where the piece is in the buffer. 50 | 51 | * `Span` represents a range in a buffer as well in the text document with a start position and length. 52 | 53 | A unit test project validates the good behavior of the implementation. Hopefully it's covering enough scenarios to make it reliable. 54 | 55 | A benchmark project helped me at identifying some flaws in the implementation and improve it. 56 | 57 | ## Benchmarks 58 | 59 | To run the benchmark, simply do in a PowerShell command prompt: 60 | 61 | ```powershell 62 | > cd Benchmarks 63 | > dotnet run -c Release 64 | ``` 65 | 66 | ### Results 67 | 68 | ```ini 69 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000 70 | 71 | AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores 72 | 73 | .NET SDK=6.0.101 74 | 75 | [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT 76 | 77 | 78 | 79 | Job=InProcess Toolchain=InProcessEmitToolchain InvocationCount=1 80 | 81 | UnrollFactor=1 82 | ``` 83 | 84 | | Method | Initial amount of pieces in the piece table | Mean | Median | Allocated | 85 | | -------------------------------------------------- | -------------------------------------------- | ---------------:| ---------------:| ------------:| 86 | | Insertion near the end of the document | 1000 | 2.126 μs | 1.900 μs | 960 B | 87 | | Insertion near the beginning of the document | 1000 | 1.857 μs | 1.600 μs | 480 B | 88 | | Insertion in the middle of the document | 1000 | 2.621 μs | 2.500 μs | 704 B | 89 | | Insertion near the end of the document | 10000 | 1.397 μs | 1.300 μs | 816 B | 90 | | Insertion near the beginning of the document | 10000 | 1.338 μs | 1.200 μs | 816 B | 91 | | Insertion in the middle of the document | 10000 | 10.286 μs | 10.300 μs | 704 B | 92 | | Insertion near the end of the document | 100000 | 3.839 μs | 3.800 μs | 816 B | 93 | | Insertion near the beginning of the document | 100000 | 4.171 μs | 3.950 μs | 1,104 B | 94 | | Insertion in the middle of the document | 100000 | 120.888 μs | 120.850 μs | 704 B | 95 | | Insertion near the end of the document | 1000000 | 5.581 μs | 5.500 μs | 816 B | 96 | | Insertion near the beginning of the document | 1000000 | 6.025 μs | 5.900 μs | 528 B | 97 | | Insertion in the middle of the document | 1000000 | 3,145.587 μs | 3,106.300 μs | 992 B | 98 | | Delete near the end of the document | 1000 | 1.851 μs | 1.800 μs | 1,248 B | 99 | | Delete near the beginning of the document | 1000 | 2.038 μs | 1.900 μs | 1,248 B | 100 | | Delete in the middle of the document | 1000 | 3.008 μs | 2.800 μs | 2,296 B | 101 | | Delete near the end of the document | 10000 | 1.444 μs | 1.300 μs | 1,584 B | 102 | | Delete near the beginning of the document | 10000 | 1.413 μs | 1.300 μs | 1,248 B | 103 | | Delete in the middle of the document | 10000 | 11.212 μs | 11.200 μs | 1,200 B | 104 | | Delete near the end of the document | 100000 | 5.727 μs | 5.700 μs | 912 B | 105 | | Delete near the beginning of the document | 100000 | 6.146 μs | 6.100 μs | 912 B | 106 | | Delete in the middle of the document | 100000 | 110.200 μs | 109.400 μs | 1,200 B | 107 | | Delete near the end of the document | 1000000 | 8.709 μs | 8.450 μs | 1,872 B | 108 | | Delete near the beginning of the document | 1000000 | 8.664 μs | 8.300 μs | 960 B | 109 | | Delete in the middle of the document | 1000000 | 2,887.207 μs | 2,852.200 μs | 624 B | 110 | | Get some text near the end of the document | 1000 | 3,467.0 ns | 2,400.0 ns | 1,080 B | 111 | | Get some text near the beginning of the document | 1000 | 2,008.1 ns | 1,800.0 ns | 1,416 B | 112 | | Get some text in the middle of the document | 1000 | 3,249.5 ns | 3,300.0 ns | 1,408 B | 113 | | Get a character near the end of the document | 1000 | 595.7 ns | 600.0 ns | - | 114 | | Get a character near the beginning of the document | 1000 | 390.9 ns | 400.0 ns | - | 115 | | Get a character in the middle of the document | 1000 | 1,563.5 ns | 1,600.0 ns | 672 B | 116 | | Get all the text in the document | 1000 | 28,003.2 ns | 27,500.0 ns | 61,424 B | 117 | | Get some text near the end of the document | 10000 | 1,840.4 ns | 1,700.0 ns | 1,472 B | 118 | | Get some text near the beginning of the document | 10000 | 1,944.9 ns | 1,800.0 ns | 792 B | 119 | | Get some text in the middle of the document | 10000 | 12,395.6 ns | 12,000.0 ns | 1,408 B | 120 | | Get a character near the end of the document | 10000 | 344.7 ns | 300.0 ns | - | 121 | | Get a character near the beginning of the document | 10000 | 401.0 ns | 400.0 ns | 624 B | 122 | | Get a character in the middle of the document | 10000 | 10,010.0 ns | 9,900.0 ns | 672 B | 123 | | Get all the text in the document | 10000 | 226,790.3 ns | 225,700.0 ns | 739,568 B | 124 | | Get some text near the end of the document | 100000 | 7,812.1 ns | 7,700.0 ns | 840 B | 125 | | Get some text near the beginning of the document | 100000 | 8,168.5 ns | 8,100.0 ns | 840 B | 126 | | Get some text in the middle of the document | 100000 | 114,848.5 ns | 112,700.0 ns | 1,120 B | 127 | | Get a character near the end of the document | 100000 | 1,102.1 ns | 1,100.0 ns | - | 128 | | Get a character near the beginning of the document | 100000 | 1,732.0 ns | 1,700.0 ns | - | 129 | | Get a character in the middle of the document | 100000 | 105,804.0 ns | 103,400.0 ns | 960 B | 130 | | Get all the text in the document | 100000 | 3,166,211.5 ns | 3,144,600.0 ns | 6,209,184 B | 131 | | Get some text near the end of the document | 1000000 | 11,806.2 ns | 10,950.0 ns | 1,128 B | 132 | | Get some text near the beginning of the document | 1000000 | 11,375.3 ns | 11,000.0 ns | 1,080 B | 133 | | Get some text in the middle of the document | 1000000 | 2,890,640.2 ns | 2,841,000.0 ns | 832 B | 134 | | Get a character near the end of the document | 1000000 | 2,253.3 ns | 2,100.0 ns | 576 B | 135 | | Get a character near the beginning of the document | 1000000 | 2,310.5 ns | 2,300.0 ns | 336 B | 136 | | Get a character in the middle of the document | 1000000 | 2,763,792.9 ns | 2,756,250.0 ns | 672 B | 137 | | Get all the text in the document | 1000000 | 37,957,515.0 ns | 38,045,400.0 ns | 53,602,496 B | 138 | 139 | ## Analysis 140 | 141 | ### Inserting or deleting at the beginning and the end of the text document is fast. 142 | 143 | That's because we're reading the table of pieces (which is a `LinkedList`) sequentially to find where does the given span / text should be inserted / deleted. Based on the location in the text document where we want to do the insertion / deletion, the program decides whether it should navigate forward (from the beginning to the end) or backward (from the end to the beginning) in the table of pieces. 144 | 145 | ### Inserting or deleting in the middle of the text document is slow. 146 | 147 | Since the table of pieces is a `LinkedList`, we have to read it sequentially. When we're trying to access to the middle of the it, reading forward or backward like explained above doesn't help. We have to pay the cost of going through more items to reach the middle. 148 | 149 | A potential way to improve it is to transform the piece table into a "piece-tree" that would basically represents the pieces in a hierarchical manner where each node also give us some information on the state of the text document. Pretty much what's explained here: https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation#_boost-line-lookup-by-using-a-balanced-binary-tree 150 | 151 | This approach should allow to perform a binary search. 152 | 153 | ### If it's slow to go through a `LinkedList`, why not using a `List`, which would allow binary search? 154 | 155 | Sure, in C#, reading the list would be faster since we can access to any item through the indexer like `myList[i]` and could perform binary search. However, assuming this data structure is used in the context of a text editor, as a user, I can typing anywhere in the document, which would require to do a `List.Insert` instead of `List.Add`. Unfortunately, `List.Insert` is very slow on a large list and internally does a copy of the list. The impact of it is that inserting and deleting in the piece table would be much slower. Assuming again that this data structure is used in the context of a text editor, this means that the typing responsiveness (how fast does the text editor answers to user type) would likely be impacted. 156 | 157 | A good StackOverlow answer about performance difference between LinkedList and List: [c# - When should I use a List vs a LinkedList - Stack Overflow](https://stackoverflow.com/a/29263914) 158 | 159 | With all that said, it might worth experimenting with other collection types like `ImmutableList` for example. But at the end of the day, a true performance improvement, in the context of a text editor, is not to do a piece table, but a "piece tree" using, for example, an AVL Tree or Red-Black Tree. 160 | 161 | ## Feedback / Contributing 162 | 163 | Found an optimization to do, a bug or a scenario not covered by unit tests? Feel free to open an issue or a pull request. 164 | -------------------------------------------------------------------------------- /UnitTests/PieceTest.cs: -------------------------------------------------------------------------------- 1 | using CsharpPieceTableImplementation; 2 | using Xunit; 3 | 4 | namespace UnitTests 5 | { 6 | public class PieceTest 7 | { 8 | [Fact] 9 | public void Piece_CanCheckEquality() 10 | { 11 | var piece 12 | = new Piece( 13 | isOriginal: true, 0, 5); 14 | 15 | var same 16 | = new Piece( 17 | isOriginal: true, 0, 5); 18 | 19 | var diffType 20 | = new Piece( 21 | isOriginal: false, 0, 5); 22 | 23 | var diffOffset 24 | = new Piece( 25 | isOriginal: true, 5, 5); 26 | 27 | var diffLength 28 | = new Piece( 29 | isOriginal: true, 0, 10); 30 | 31 | Assert.True(piece.Equals(same)); 32 | Assert.False(piece.Equals(diffType)); 33 | Assert.False(piece.Equals(diffOffset)); 34 | Assert.False(piece.Equals(diffLength)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /UnitTests/TextDocumentBufferTest.cs: -------------------------------------------------------------------------------- 1 | using CsharpPieceTableImplementation; 2 | using System; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace UnitTests 7 | { 8 | public class TextDocumentBufferTest 9 | { 10 | private const string Text 11 | = "During the development of the .NET Framework, the class libraries were originally written using a managed code compiler system called \"Simple Managed C\" (SMC)."; 12 | 13 | private TextDocumentBuffer _textDocumentBuffer; 14 | 15 | public TextDocumentBufferTest() 16 | { 17 | _textDocumentBuffer 18 | = new TextDocumentBuffer( 19 | Text.ToArray()); 20 | } 21 | 22 | [Fact] 23 | public void DocumentLength_OriginalText() 24 | { 25 | Assert.Equal(Text.Length, _textDocumentBuffer.DocumentLength); 26 | } 27 | 28 | [Fact] 29 | public void GetCharacter_OriginalDocumentUnchanged() 30 | { 31 | Assert.Equal(Text[0], _textDocumentBuffer[0]); 32 | Assert.Equal(Text[^1], _textDocumentBuffer[_textDocumentBuffer.DocumentLength - 1]); 33 | } 34 | 35 | [Fact] 36 | public void GetText_FullOriginalDocumentUnchanged() 37 | { 38 | Assert.Equal(Text, GetFullDocument()); 39 | } 40 | 41 | [Fact] 42 | public void GetText_ExtractBeginningOfOriginalDocumentUnchanged() 43 | { 44 | string documentText = _textDocumentBuffer.GetText(new Span(0, 2)); 45 | Assert.Equal(Text[..2], documentText); 46 | } 47 | 48 | [Fact] 49 | public void GetText_ExtractEndOfOriginalDocumentUnchanged() 50 | { 51 | string documentText = _textDocumentBuffer.GetText(new Span(_textDocumentBuffer.DocumentLength - 2, 2)); 52 | Assert.Equal(Text.Substring(Text.Length - 2, 2), documentText); 53 | } 54 | 55 | [Fact] 56 | public void GetText_ExtractMiddleOfOriginalDocumentUnchanged() 57 | { 58 | string documentText = _textDocumentBuffer.GetText(new Span(1, 2)); 59 | Assert.Equal(Text.Substring(1, 2), documentText); 60 | } 61 | 62 | [Fact] 63 | public void Insert_EmptyDocument() 64 | { 65 | _textDocumentBuffer 66 | = new TextDocumentBuffer(Array.Empty()); 67 | 68 | string appendText = "TEST!"; 69 | _textDocumentBuffer.Insert(0, appendText); 70 | 71 | Assert.Equal(appendText.Length, _textDocumentBuffer.DocumentLength); 72 | Assert.Equal(appendText, GetFullDocument()); 73 | } 74 | 75 | [Fact] 76 | public void Insert_EndOfDocument() 77 | { 78 | string appendText = "TEST!"; 79 | _textDocumentBuffer.Insert(Text.Length, appendText); 80 | 81 | Assert.Equal(Text.Length + appendText.Length, _textDocumentBuffer.DocumentLength); 82 | Assert.Equal(Text + appendText, GetFullDocument()); 83 | } 84 | 85 | [Fact] 86 | public void Insert_BeginningOfDocument() 87 | { 88 | string appendText = "TEST!"; 89 | _textDocumentBuffer.Insert(0, appendText); 90 | 91 | Assert.Equal(appendText.Length + Text.Length, _textDocumentBuffer.DocumentLength); 92 | Assert.Equal(appendText + Text, GetFullDocument()); 93 | } 94 | 95 | [Fact] 96 | public void Insert_InsideOfOriginalDocument() 97 | { 98 | string appendText = "TEST!"; 99 | _textDocumentBuffer.Insert(2, appendText); 100 | 101 | Assert.Equal(appendText.Length + Text.Length, _textDocumentBuffer.DocumentLength); 102 | Assert.Equal(string.Concat(Text.AsSpan(0, 2), appendText, Text.AsSpan(2)), GetFullDocument()); 103 | } 104 | 105 | [Fact] 106 | public void Insert_BeginningOfAppendBufferPiece() 107 | { 108 | string appendText = "TEST!"; 109 | _textDocumentBuffer.Insert(2, appendText); 110 | 111 | string appendText2 = "HelloThere"; 112 | _textDocumentBuffer.Insert(2, appendText2); 113 | 114 | Assert.Equal(appendText.Length + appendText2.Length + Text.Length, _textDocumentBuffer.DocumentLength); 115 | Assert.Equal(string.Concat(Text.AsSpan(0, 2), appendText2, appendText, Text.AsSpan(2)), GetFullDocument()); 116 | } 117 | 118 | [Fact] 119 | public void Insert_EndOfAppendBufferPiece() 120 | { 121 | string appendText = "TEST!"; 122 | _textDocumentBuffer.Insert(2, appendText); 123 | 124 | string appendText2 = "HelloThere"; 125 | _textDocumentBuffer.Insert(2 + appendText.Length, appendText2); 126 | 127 | Assert.Equal(appendText.Length + appendText2.Length + Text.Length, _textDocumentBuffer.DocumentLength); 128 | Assert.Equal(Text[..2] + appendText + appendText2 + Text[2..], GetFullDocument()); 129 | } 130 | 131 | [Fact] 132 | public void Insert_InsideOfAppendBufferPiece() 133 | { 134 | string appendText = "TEST!"; 135 | _textDocumentBuffer.Insert(2, appendText); 136 | 137 | string appendText2 = "HelloThere"; 138 | _textDocumentBuffer.Insert(4, appendText2); 139 | 140 | Assert.Equal(appendText.Length + appendText2.Length + Text.Length, _textDocumentBuffer.DocumentLength); 141 | Assert.Equal(Text[..2] + appendText[..2] + appendText2 + appendText.Substring(2, 3) + Text[2..], GetFullDocument()); 142 | } 143 | 144 | [Fact] 145 | public void Delete_BeginningOfDocument() 146 | { 147 | // Remove the first 2 characters of the document. 148 | _textDocumentBuffer.Delete(new Span(0, 2)); 149 | 150 | Assert.Equal(Text.Length - 2, _textDocumentBuffer.DocumentLength); 151 | Assert.Equal(Text[2..], GetFullDocument()); 152 | } 153 | 154 | [Fact] 155 | public void Delete_EndOfDocument() 156 | { 157 | // Remove the last 2 characters of the document. 158 | _textDocumentBuffer.Delete(new Span(_textDocumentBuffer.DocumentLength - 2, 2)); 159 | 160 | Assert.Equal(Text.Length - 2, _textDocumentBuffer.DocumentLength); 161 | Assert.Equal(Text[0..^2], GetFullDocument()); 162 | } 163 | 164 | [Fact] 165 | public void Delete_InsideOfOriginalDocument() 166 | { 167 | _textDocumentBuffer.Delete(new Span(1, 2)); 168 | 169 | Assert.Equal(Text.Length - 2, _textDocumentBuffer.DocumentLength); 170 | Assert.Equal(Text[..1] + Text[3..], GetFullDocument()); 171 | } 172 | 173 | [Fact] 174 | public void Delete_BeginningOfAppendBufferPiece() 175 | { 176 | string appendText = "TEST!"; 177 | _textDocumentBuffer.Insert(2, appendText); 178 | 179 | // Delete "TE". 180 | _textDocumentBuffer.Delete(new Span(2, 2)); 181 | 182 | Assert.Equal(Text.Length + appendText.Length - 2, _textDocumentBuffer.DocumentLength); 183 | Assert.Equal(Text[..2] + appendText[2..] + Text[2..], GetFullDocument()); 184 | } 185 | 186 | [Fact] 187 | public void Delete_EndOfAppendBufferPiece() 188 | { 189 | string appendText = "TEST!"; 190 | _textDocumentBuffer.Insert(2, appendText); 191 | 192 | // Delete "ST!". 193 | _textDocumentBuffer.Delete(new Span(2 + appendText.Length - 3, 3)); 194 | 195 | Assert.Equal(Text.Length + appendText.Length - 3, _textDocumentBuffer.DocumentLength); 196 | Assert.Equal(Text[..2] + appendText[..2] + Text[2..], GetFullDocument()); 197 | } 198 | 199 | [Fact] 200 | public void Delete_InsideOfAppendBufferPiece() 201 | { 202 | string appendText = "TEST!"; 203 | _textDocumentBuffer.Insert(2, appendText); 204 | 205 | // Delete "ES". 206 | _textDocumentBuffer.Delete(new Span(3, 2)); 207 | 208 | Assert.Equal(Text.Length + appendText.Length - 2, _textDocumentBuffer.DocumentLength); 209 | Assert.Equal(Text[..2] + appendText[..1] + appendText[3..] + Text[2..], GetFullDocument()); 210 | } 211 | 212 | [Fact] 213 | public void Delete_AccrossSeveralAppendBufferPieces() 214 | { 215 | string appendText = "Hello_"; 216 | _textDocumentBuffer.Insert(2, appendText); 217 | 218 | string appendText2 = "World!/"; 219 | _textDocumentBuffer.Insert(2 + appendText.Length, appendText2); 220 | 221 | string appendText3 = "Boo"; 222 | _textDocumentBuffer.Insert(2 + appendText.Length + appendText2.Length, appendText3); 223 | 224 | string appendText4 = "Foo Bar"; 225 | _textDocumentBuffer.Insert(2 + appendText.Length + appendText2.Length + appendText3.Length, appendText4); 226 | 227 | // Delete "_World!/BooFoo", so it forms "Hello Bar". 228 | _textDocumentBuffer.Delete(new Span(2 + "Hello".Length, "_".Length + appendText2.Length + appendText3.Length + "Foo".Length)); 229 | 230 | Assert.Equal(Text.Length + "Hello Bar".Length, _textDocumentBuffer.DocumentLength); 231 | Assert.Equal(Text[..2] + appendText[..5] + appendText4[3..] + Text[2..], GetFullDocument()); 232 | } 233 | 234 | [Fact] 235 | public void Delete_AccrossSeveralPiecesOfVariousBuffer() 236 | { 237 | string originalText = "Hello!"; 238 | _textDocumentBuffer 239 | = new TextDocumentBuffer( 240 | originalText.ToArray()); 241 | Assert.Equal("Hello!", GetFullDocument()); 242 | 243 | string appendText = " I'm testing a PieceTable implementation."; 244 | _textDocumentBuffer.Insert(originalText.Length, appendText); 245 | Assert.Equal("Hello! I'm testing a PieceTable implementation.", GetFullDocument()); 246 | 247 | string appendText2 = " there"; 248 | _textDocumentBuffer.Insert("Hello".Length, appendText2); 249 | Assert.Equal("Hello there! I'm testing a PieceTable implementation.", GetFullDocument()); 250 | 251 | _textDocumentBuffer.Delete(new Span(0, "Hello there! ".Length)); 252 | 253 | Assert.Equal("I'm testing a PieceTable implementation.".Length, _textDocumentBuffer.DocumentLength); 254 | Assert.Equal("I'm testing a PieceTable implementation.", GetFullDocument()); 255 | } 256 | 257 | [Fact] 258 | public void InsertAndDelete() 259 | { 260 | Delete_AccrossSeveralPiecesOfVariousBuffer(); 261 | Assert.Equal("I'm testing a PieceTable implementation.", GetFullDocument()); 262 | 263 | _textDocumentBuffer.Insert("I'm testing a".Length, "n implementation of"); 264 | Assert.Equal("I'm testing an implementation of PieceTable implementation.", GetFullDocument()); 265 | 266 | _textDocumentBuffer.Delete(new Span("I'm testing an implementation of PieceTable ".Length, "implementation".Length)); 267 | Assert.Equal("I'm testing an implementation of PieceTable .", GetFullDocument()); 268 | 269 | _textDocumentBuffer.Insert("I'm testing an implementation of PieceTable ".Length, "data Structure"); 270 | Assert.Equal("I'm testing an implementation of PieceTable data Structure.", GetFullDocument()); 271 | 272 | _textDocumentBuffer.Delete(new Span("I'm testing an implementation of PieceTable data ".Length, "S".Length)); 273 | Assert.Equal("I'm testing an implementation of PieceTable data tructure.", GetFullDocument()); 274 | 275 | _textDocumentBuffer.Insert("I'm testing an implementation of PieceTable data ".Length, 's'); 276 | 277 | Assert.Equal("I'm testing an implementation of PieceTable data structure.".Length, _textDocumentBuffer.DocumentLength); 278 | Assert.Equal("I'm testing an implementation of PieceTable data structure.", GetFullDocument()); 279 | } 280 | 281 | [Fact] 282 | public void Insert_Pressure() 283 | { 284 | int max = 10_000_000; 285 | 286 | string expectedResult = Text + new string('a', max); 287 | 288 | for (int i = 0; i < max; i++) 289 | { 290 | _textDocumentBuffer.Insert(_textDocumentBuffer.DocumentLength, 'a'); 291 | } 292 | 293 | Assert.Equal(expectedResult, GetFullDocument()); 294 | 295 | for (int i = 0; i < max + Text.Length; i++) 296 | { 297 | _textDocumentBuffer.Delete(new Span(0, 1)); 298 | } 299 | 300 | Assert.Equal(string.Empty, GetFullDocument()); 301 | } 302 | 303 | private string GetFullDocument() 304 | { 305 | string documentText = _textDocumentBuffer.GetText(new Span(0, _textDocumentBuffer.DocumentLength)); 306 | return documentText; 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | disable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------