├── .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 |
--------------------------------------------------------------------------------