1 | // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team |
---|
2 | // |
---|
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this |
---|
4 | // software and associated documentation files (the "Software"), to deal in the Software |
---|
5 | // without restriction, including without limitation the rights to use, copy, modify, merge, |
---|
6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
---|
7 | // to whom the Software is furnished to do so, subject to the following conditions: |
---|
8 | // |
---|
9 | // The above copyright notice and this permission notice shall be included in all copies or |
---|
10 | // substantial portions of the Software. |
---|
11 | // |
---|
12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
---|
13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
---|
14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
---|
15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
---|
16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
---|
17 | // DEALINGS IN THE SOFTWARE. |
---|
18 | |
---|
19 | using System; |
---|
20 | using System.Collections.Generic; |
---|
21 | using System.Collections.ObjectModel; |
---|
22 | using System.ComponentModel; |
---|
23 | using System.ComponentModel.Design; |
---|
24 | using System.Diagnostics; |
---|
25 | using System.Globalization; |
---|
26 | using System.Threading; |
---|
27 | using ICSharpCode.AvalonEdit.Utils; |
---|
28 | using ICSharpCode.NRefactory; |
---|
29 | using ICSharpCode.NRefactory.Editor; |
---|
30 | |
---|
31 | namespace ICSharpCode.AvalonEdit.Document |
---|
32 | { |
---|
33 | /// <summary> |
---|
34 | /// This class is the main class of the text model. Basically, it is a <see cref="System.Text.StringBuilder"/> with events. |
---|
35 | /// </summary> |
---|
36 | /// <remarks> |
---|
37 | /// <b>Thread safety:</b> |
---|
38 | /// <inheritdoc cref="VerifyAccess"/> |
---|
39 | /// <para>However, there is a single method that is thread-safe: <see cref="CreateSnapshot()"/> (and its overloads).</para> |
---|
40 | /// </remarks> |
---|
41 | public sealed class TextDocument : IDocument, INotifyPropertyChanged |
---|
42 | { |
---|
43 | #region Thread ownership |
---|
44 | readonly object lockObject = new object(); |
---|
45 | Thread owner = Thread.CurrentThread; |
---|
46 | |
---|
47 | /// <summary> |
---|
48 | /// Verifies that the current thread is the documents owner thread. |
---|
49 | /// Throws an <see cref="InvalidOperationException"/> if the wrong thread accesses the TextDocument. |
---|
50 | /// </summary> |
---|
51 | /// <remarks> |
---|
52 | /// <para>The TextDocument class is not thread-safe. A document instance expects to have a single owner thread |
---|
53 | /// and will throw an <see cref="InvalidOperationException"/> when accessed from another thread. |
---|
54 | /// It is possible to change the owner thread using the <see cref="SetOwnerThread"/> method.</para> |
---|
55 | /// </remarks> |
---|
56 | public void VerifyAccess() |
---|
57 | { |
---|
58 | if (Thread.CurrentThread != owner) |
---|
59 | throw new InvalidOperationException("TextDocument can be accessed only from the thread that owns it."); |
---|
60 | } |
---|
61 | |
---|
62 | /// <summary> |
---|
63 | /// Transfers ownership of the document to another thread. This method can be used to load |
---|
64 | /// a file into a TextDocument on a background thread and then transfer ownership to the UI thread |
---|
65 | /// for displaying the document. |
---|
66 | /// </summary> |
---|
67 | /// <remarks> |
---|
68 | /// <inheritdoc cref="VerifyAccess"/> |
---|
69 | /// <para> |
---|
70 | /// The owner can be set to null, which means that no thread can access the document. But, if the document |
---|
71 | /// has no owner thread, any thread may take ownership by calling <see cref="SetOwnerThread"/>. |
---|
72 | /// </para> |
---|
73 | /// </remarks> |
---|
74 | public void SetOwnerThread(Thread newOwner) |
---|
75 | { |
---|
76 | // We need to lock here to ensure that in the null owner case, |
---|
77 | // only one thread succeeds in taking ownership. |
---|
78 | lock (lockObject) { |
---|
79 | if (owner != null) { |
---|
80 | VerifyAccess(); |
---|
81 | } |
---|
82 | owner = newOwner; |
---|
83 | } |
---|
84 | } |
---|
85 | #endregion |
---|
86 | |
---|
87 | #region Fields + Constructor |
---|
88 | readonly Rope<char> rope; |
---|
89 | readonly DocumentLineTree lineTree; |
---|
90 | readonly LineManager lineManager; |
---|
91 | readonly TextAnchorTree anchorTree; |
---|
92 | readonly TextSourceVersionProvider versionProvider = new TextSourceVersionProvider(); |
---|
93 | |
---|
94 | /// <summary> |
---|
95 | /// Create an empty text document. |
---|
96 | /// </summary> |
---|
97 | public TextDocument() |
---|
98 | : this(string.Empty) |
---|
99 | { |
---|
100 | } |
---|
101 | |
---|
102 | /// <summary> |
---|
103 | /// Create a new text document with the specified initial text. |
---|
104 | /// </summary> |
---|
105 | public TextDocument(IEnumerable<char> initialText) |
---|
106 | { |
---|
107 | if (initialText == null) |
---|
108 | throw new ArgumentNullException("initialText"); |
---|
109 | rope = new Rope<char>(initialText); |
---|
110 | lineTree = new DocumentLineTree(this); |
---|
111 | lineManager = new LineManager(lineTree, this); |
---|
112 | lineTrackers.CollectionChanged += delegate { |
---|
113 | lineManager.UpdateListOfLineTrackers(); |
---|
114 | }; |
---|
115 | |
---|
116 | anchorTree = new TextAnchorTree(this); |
---|
117 | undoStack = new UndoStack(); |
---|
118 | FireChangeEvents(); |
---|
119 | } |
---|
120 | |
---|
121 | /// <summary> |
---|
122 | /// Create a new text document with the specified initial text. |
---|
123 | /// </summary> |
---|
124 | public TextDocument(ITextSource initialText) |
---|
125 | : this(GetTextFromTextSource(initialText)) |
---|
126 | { |
---|
127 | } |
---|
128 | |
---|
129 | // gets the text from a text source, directly retrieving the underlying rope where possible |
---|
130 | static IEnumerable<char> GetTextFromTextSource(ITextSource textSource) |
---|
131 | { |
---|
132 | if (textSource == null) |
---|
133 | throw new ArgumentNullException("textSource"); |
---|
134 | |
---|
135 | #if NREFACTORY |
---|
136 | if (textSource is ReadOnlyDocument) |
---|
137 | textSource = textSource.CreateSnapshot(); // retrieve underlying text source, which might be a RopeTextSource |
---|
138 | #endif |
---|
139 | |
---|
140 | RopeTextSource rts = textSource as RopeTextSource; |
---|
141 | if (rts != null) |
---|
142 | return rts.GetRope(); |
---|
143 | |
---|
144 | TextDocument doc = textSource as TextDocument; |
---|
145 | if (doc != null) |
---|
146 | return doc.rope; |
---|
147 | |
---|
148 | return textSource.Text; |
---|
149 | } |
---|
150 | #endregion |
---|
151 | |
---|
152 | #region Text |
---|
153 | void ThrowIfRangeInvalid(int offset, int length) |
---|
154 | { |
---|
155 | if (offset < 0 || offset > rope.Length) { |
---|
156 | throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture)); |
---|
157 | } |
---|
158 | if (length < 0 || offset + length > rope.Length) { |
---|
159 | throw new ArgumentOutOfRangeException("length", length, "0 <= length, offset(" + offset + ")+length <= " + rope.Length.ToString(CultureInfo.InvariantCulture)); |
---|
160 | } |
---|
161 | } |
---|
162 | |
---|
163 | /// <inheritdoc/> |
---|
164 | public string GetText(int offset, int length) |
---|
165 | { |
---|
166 | VerifyAccess(); |
---|
167 | return rope.ToString(offset, length); |
---|
168 | } |
---|
169 | |
---|
170 | /// <summary> |
---|
171 | /// Retrieves the text for a portion of the document. |
---|
172 | /// </summary> |
---|
173 | public string GetText(ISegment segment) |
---|
174 | { |
---|
175 | if (segment == null) |
---|
176 | throw new ArgumentNullException("segment"); |
---|
177 | return GetText(segment.Offset, segment.Length); |
---|
178 | } |
---|
179 | |
---|
180 | /// <inheritdoc/> |
---|
181 | public int IndexOf(char c, int startIndex, int count) |
---|
182 | { |
---|
183 | DebugVerifyAccess(); |
---|
184 | return rope.IndexOf(c, startIndex, count); |
---|
185 | } |
---|
186 | |
---|
187 | /// <inheritdoc/> |
---|
188 | public int LastIndexOf(char c, int startIndex, int count) |
---|
189 | { |
---|
190 | DebugVerifyAccess(); |
---|
191 | return rope.LastIndexOf(c, startIndex, count); |
---|
192 | } |
---|
193 | |
---|
194 | /// <inheritdoc/> |
---|
195 | public int IndexOfAny(char[] anyOf, int startIndex, int count) |
---|
196 | { |
---|
197 | DebugVerifyAccess(); // frequently called (NewLineFinder), so must be fast in release builds |
---|
198 | return rope.IndexOfAny(anyOf, startIndex, count); |
---|
199 | } |
---|
200 | |
---|
201 | /// <inheritdoc/> |
---|
202 | public int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
---|
203 | { |
---|
204 | DebugVerifyAccess(); |
---|
205 | return rope.IndexOf(searchText, startIndex, count, comparisonType); |
---|
206 | } |
---|
207 | |
---|
208 | /// <inheritdoc/> |
---|
209 | public int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType) |
---|
210 | { |
---|
211 | DebugVerifyAccess(); |
---|
212 | return rope.LastIndexOf(searchText, startIndex, count, comparisonType); |
---|
213 | } |
---|
214 | |
---|
215 | /// <inheritdoc/> |
---|
216 | public char GetCharAt(int offset) |
---|
217 | { |
---|
218 | DebugVerifyAccess(); // frequently called, so must be fast in release builds |
---|
219 | return rope[offset]; |
---|
220 | } |
---|
221 | |
---|
222 | WeakReference cachedText; |
---|
223 | |
---|
224 | /// <summary> |
---|
225 | /// Gets/Sets the text of the whole document. |
---|
226 | /// </summary> |
---|
227 | public string Text { |
---|
228 | get { |
---|
229 | VerifyAccess(); |
---|
230 | string completeText = cachedText != null ? (cachedText.Target as string) : null; |
---|
231 | if (completeText == null) { |
---|
232 | completeText = rope.ToString(); |
---|
233 | cachedText = new WeakReference(completeText); |
---|
234 | } |
---|
235 | return completeText; |
---|
236 | } |
---|
237 | set { |
---|
238 | VerifyAccess(); |
---|
239 | if (value == null) |
---|
240 | throw new ArgumentNullException("value"); |
---|
241 | Replace(0, rope.Length, value); |
---|
242 | } |
---|
243 | } |
---|
244 | |
---|
245 | /// <inheritdoc/> |
---|
246 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
247 | public event EventHandler TextChanged; |
---|
248 | |
---|
249 | event EventHandler IDocument.ChangeCompleted { |
---|
250 | add { this.TextChanged += value; } |
---|
251 | remove { this.TextChanged -= value; } |
---|
252 | } |
---|
253 | |
---|
254 | /// <inheritdoc/> |
---|
255 | public int TextLength { |
---|
256 | get { |
---|
257 | VerifyAccess(); |
---|
258 | return rope.Length; |
---|
259 | } |
---|
260 | } |
---|
261 | |
---|
262 | /// <summary> |
---|
263 | /// Is raised when the TextLength property changes. |
---|
264 | /// </summary> |
---|
265 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
266 | [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")] |
---|
267 | public event EventHandler TextLengthChanged; |
---|
268 | |
---|
269 | /// <summary> |
---|
270 | /// Is raised when one of the properties <see cref="Text"/>, <see cref="TextLength"/>, <see cref="LineCount"/>, |
---|
271 | /// <see cref="UndoStack"/> changes. |
---|
272 | /// </summary> |
---|
273 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
274 | public event PropertyChangedEventHandler PropertyChanged; |
---|
275 | |
---|
276 | /// <summary> |
---|
277 | /// Is raised before the document changes. |
---|
278 | /// </summary> |
---|
279 | /// <remarks> |
---|
280 | /// <para>Here is the order in which events are raised during a document update:</para> |
---|
281 | /// <list type="bullet"> |
---|
282 | /// <item><description><b><see cref="BeginUpdate">BeginUpdate()</see></b></description> |
---|
283 | /// <list type="bullet"> |
---|
284 | /// <item><description>Start of change group (on undo stack)</description></item> |
---|
285 | /// <item><description><see cref="UpdateStarted"/> event is raised</description></item> |
---|
286 | /// </list></item> |
---|
287 | /// <item><description><b><see cref="Insert(int,string)">Insert()</see> / <see cref="Remove(int,int)">Remove()</see> / <see cref="Replace(int,int,string)">Replace()</see></b></description> |
---|
288 | /// <list type="bullet"> |
---|
289 | /// <item><description><see cref="Changing"/> event is raised</description></item> |
---|
290 | /// <item><description>The document is changed</description></item> |
---|
291 | /// <item><description><see cref="TextAnchor.Deleted">TextAnchor.Deleted</see> event is raised if anchors were |
---|
292 | /// in the deleted text portion</description></item> |
---|
293 | /// <item><description><see cref="Changed"/> event is raised</description></item> |
---|
294 | /// </list></item> |
---|
295 | /// <item><description><b><see cref="EndUpdate">EndUpdate()</see></b></description> |
---|
296 | /// <list type="bullet"> |
---|
297 | /// <item><description><see cref="TextChanged"/> event is raised</description></item> |
---|
298 | /// <item><description><see cref="PropertyChanged"/> event is raised (for the Text, TextLength, LineCount properties, in that order)</description></item> |
---|
299 | /// <item><description>End of change group (on undo stack)</description></item> |
---|
300 | /// <item><description><see cref="UpdateFinished"/> event is raised</description></item> |
---|
301 | /// </list></item> |
---|
302 | /// </list> |
---|
303 | /// <para> |
---|
304 | /// If the insert/remove/replace methods are called without a call to <c>BeginUpdate()</c>, |
---|
305 | /// they will call <c>BeginUpdate()</c> and <c>EndUpdate()</c> to ensure no change happens outside of <c>UpdateStarted</c>/<c>UpdateFinished</c>. |
---|
306 | /// </para><para> |
---|
307 | /// There can be multiple document changes between the <c>BeginUpdate()</c> and <c>EndUpdate()</c> calls. |
---|
308 | /// In this case, the events associated with EndUpdate will be raised only once after the whole document update is done. |
---|
309 | /// </para><para> |
---|
310 | /// The <see cref="UndoStack"/> listens to the <c>UpdateStarted</c> and <c>UpdateFinished</c> events to group all changes into a single undo step. |
---|
311 | /// </para> |
---|
312 | /// </remarks> |
---|
313 | public event EventHandler<DocumentChangeEventArgs> Changing; |
---|
314 | |
---|
315 | // Unfortunately EventHandler<T> is invariant, so we have to use two separate events |
---|
316 | private event EventHandler<TextChangeEventArgs> textChanging; |
---|
317 | |
---|
318 | event EventHandler<TextChangeEventArgs> IDocument.TextChanging { |
---|
319 | add { textChanging += value; } |
---|
320 | remove { textChanging -= value; } |
---|
321 | } |
---|
322 | |
---|
323 | /// <summary> |
---|
324 | /// Is raised after the document has changed. |
---|
325 | /// </summary> |
---|
326 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
327 | public event EventHandler<DocumentChangeEventArgs> Changed; |
---|
328 | |
---|
329 | private event EventHandler<TextChangeEventArgs> textChanged; |
---|
330 | |
---|
331 | event EventHandler<TextChangeEventArgs> IDocument.TextChanged { |
---|
332 | add { textChanged += value; } |
---|
333 | remove { textChanged -= value; } |
---|
334 | } |
---|
335 | |
---|
336 | /// <summary> |
---|
337 | /// Creates a snapshot of the current text. |
---|
338 | /// </summary> |
---|
339 | /// <remarks> |
---|
340 | /// <para>This method returns an immutable snapshot of the document, and may be safely called even when |
---|
341 | /// the document's owner thread is concurrently modifying the document. |
---|
342 | /// </para><para> |
---|
343 | /// This special thread-safety guarantee is valid only for TextDocument.CreateSnapshot(), not necessarily for other |
---|
344 | /// classes implementing ITextSource.CreateSnapshot(). |
---|
345 | /// </para><para> |
---|
346 | /// </para> |
---|
347 | /// </remarks> |
---|
348 | public ITextSource CreateSnapshot() |
---|
349 | { |
---|
350 | lock (lockObject) { |
---|
351 | return new RopeTextSource(rope, versionProvider.CurrentVersion); |
---|
352 | } |
---|
353 | } |
---|
354 | |
---|
355 | /// <summary> |
---|
356 | /// Creates a snapshot of a part of the current text. |
---|
357 | /// </summary> |
---|
358 | /// <remarks><inheritdoc cref="CreateSnapshot()"/></remarks> |
---|
359 | public ITextSource CreateSnapshot(int offset, int length) |
---|
360 | { |
---|
361 | lock (lockObject) { |
---|
362 | return new RopeTextSource(rope.GetRange(offset, length)); |
---|
363 | } |
---|
364 | } |
---|
365 | |
---|
366 | #if NREFACTORY |
---|
367 | /// <inheritdoc/> |
---|
368 | public IDocument CreateDocumentSnapshot() |
---|
369 | { |
---|
370 | return new ReadOnlyDocument(this, fileName); |
---|
371 | } |
---|
372 | #endif |
---|
373 | |
---|
374 | /// <inheritdoc/> |
---|
375 | public ITextSourceVersion Version { |
---|
376 | get { return versionProvider.CurrentVersion; } |
---|
377 | } |
---|
378 | |
---|
379 | /// <inheritdoc/> |
---|
380 | public System.IO.TextReader CreateReader() |
---|
381 | { |
---|
382 | lock (lockObject) { |
---|
383 | return new RopeTextReader(rope); |
---|
384 | } |
---|
385 | } |
---|
386 | |
---|
387 | /// <inheritdoc/> |
---|
388 | public System.IO.TextReader CreateReader(int offset, int length) |
---|
389 | { |
---|
390 | lock (lockObject) { |
---|
391 | return new RopeTextReader(rope.GetRange(offset, length)); |
---|
392 | } |
---|
393 | } |
---|
394 | |
---|
395 | /// <inheritdoc/> |
---|
396 | public void WriteTextTo(System.IO.TextWriter writer) |
---|
397 | { |
---|
398 | VerifyAccess(); |
---|
399 | rope.WriteTo(writer, 0, rope.Length); |
---|
400 | } |
---|
401 | |
---|
402 | /// <inheritdoc/> |
---|
403 | public void WriteTextTo(System.IO.TextWriter writer, int offset, int length) |
---|
404 | { |
---|
405 | VerifyAccess(); |
---|
406 | rope.WriteTo(writer, offset, length); |
---|
407 | } |
---|
408 | #endregion |
---|
409 | |
---|
410 | #region BeginUpdate / EndUpdate |
---|
411 | int beginUpdateCount; |
---|
412 | |
---|
413 | /// <summary> |
---|
414 | /// Gets if an update is running. |
---|
415 | /// </summary> |
---|
416 | /// <remarks><inheritdoc cref="BeginUpdate"/></remarks> |
---|
417 | public bool IsInUpdate { |
---|
418 | get { |
---|
419 | VerifyAccess(); |
---|
420 | return beginUpdateCount > 0; |
---|
421 | } |
---|
422 | } |
---|
423 | |
---|
424 | /// <summary> |
---|
425 | /// Immediately calls <see cref="BeginUpdate()"/>, |
---|
426 | /// and returns an IDisposable that calls <see cref="EndUpdate()"/>. |
---|
427 | /// </summary> |
---|
428 | /// <remarks><inheritdoc cref="BeginUpdate"/></remarks> |
---|
429 | public IDisposable RunUpdate() |
---|
430 | { |
---|
431 | BeginUpdate(); |
---|
432 | return new CallbackOnDispose(EndUpdate); |
---|
433 | } |
---|
434 | |
---|
435 | /// <summary> |
---|
436 | /// <para>Begins a group of document changes.</para> |
---|
437 | /// <para>Some events are suspended until EndUpdate is called, and the <see cref="UndoStack"/> will |
---|
438 | /// group all changes into a single action.</para> |
---|
439 | /// <para>Calling BeginUpdate several times increments a counter, only after the appropriate number |
---|
440 | /// of EndUpdate calls the events resume their work.</para> |
---|
441 | /// </summary> |
---|
442 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
443 | public void BeginUpdate() |
---|
444 | { |
---|
445 | VerifyAccess(); |
---|
446 | if (inDocumentChanging) |
---|
447 | throw new InvalidOperationException("Cannot change document within another document change."); |
---|
448 | beginUpdateCount++; |
---|
449 | if (beginUpdateCount == 1) { |
---|
450 | undoStack.StartUndoGroup(); |
---|
451 | if (UpdateStarted != null) |
---|
452 | UpdateStarted(this, EventArgs.Empty); |
---|
453 | } |
---|
454 | } |
---|
455 | |
---|
456 | /// <summary> |
---|
457 | /// Ends a group of document changes. |
---|
458 | /// </summary> |
---|
459 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
460 | public void EndUpdate() |
---|
461 | { |
---|
462 | VerifyAccess(); |
---|
463 | if (inDocumentChanging) |
---|
464 | throw new InvalidOperationException("Cannot end update within document change."); |
---|
465 | if (beginUpdateCount == 0) |
---|
466 | throw new InvalidOperationException("No update is active."); |
---|
467 | if (beginUpdateCount == 1) { |
---|
468 | // fire change events inside the change group - event handlers might add additional |
---|
469 | // document changes to the change group |
---|
470 | FireChangeEvents(); |
---|
471 | undoStack.EndUndoGroup(); |
---|
472 | beginUpdateCount = 0; |
---|
473 | if (UpdateFinished != null) |
---|
474 | UpdateFinished(this, EventArgs.Empty); |
---|
475 | } else { |
---|
476 | beginUpdateCount -= 1; |
---|
477 | } |
---|
478 | } |
---|
479 | |
---|
480 | /// <summary> |
---|
481 | /// Occurs when a document change starts. |
---|
482 | /// </summary> |
---|
483 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
484 | public event EventHandler UpdateStarted; |
---|
485 | |
---|
486 | /// <summary> |
---|
487 | /// Occurs when a document change is finished. |
---|
488 | /// </summary> |
---|
489 | /// <remarks><inheritdoc cref="Changing"/></remarks> |
---|
490 | public event EventHandler UpdateFinished; |
---|
491 | |
---|
492 | void IDocument.StartUndoableAction() |
---|
493 | { |
---|
494 | BeginUpdate(); |
---|
495 | } |
---|
496 | |
---|
497 | void IDocument.EndUndoableAction() |
---|
498 | { |
---|
499 | EndUpdate(); |
---|
500 | } |
---|
501 | |
---|
502 | IDisposable IDocument.OpenUndoGroup() |
---|
503 | { |
---|
504 | return RunUpdate(); |
---|
505 | } |
---|
506 | #endregion |
---|
507 | |
---|
508 | #region Fire events after update |
---|
509 | int oldTextLength; |
---|
510 | int oldLineCount; |
---|
511 | bool fireTextChanged; |
---|
512 | |
---|
513 | /// <summary> |
---|
514 | /// Fires TextChanged, TextLengthChanged, LineCountChanged if required. |
---|
515 | /// </summary> |
---|
516 | internal void FireChangeEvents() |
---|
517 | { |
---|
518 | // it may be necessary to fire the event multiple times if the document is changed |
---|
519 | // from inside the event handlers |
---|
520 | while (fireTextChanged) { |
---|
521 | fireTextChanged = false; |
---|
522 | if (TextChanged != null) |
---|
523 | TextChanged(this, EventArgs.Empty); |
---|
524 | OnPropertyChanged("Text"); |
---|
525 | |
---|
526 | int textLength = rope.Length; |
---|
527 | if (textLength != oldTextLength) { |
---|
528 | oldTextLength = textLength; |
---|
529 | if (TextLengthChanged != null) |
---|
530 | TextLengthChanged(this, EventArgs.Empty); |
---|
531 | OnPropertyChanged("TextLength"); |
---|
532 | } |
---|
533 | int lineCount = lineTree.LineCount; |
---|
534 | if (lineCount != oldLineCount) { |
---|
535 | oldLineCount = lineCount; |
---|
536 | if (LineCountChanged != null) |
---|
537 | LineCountChanged(this, EventArgs.Empty); |
---|
538 | OnPropertyChanged("LineCount"); |
---|
539 | } |
---|
540 | } |
---|
541 | } |
---|
542 | |
---|
543 | void OnPropertyChanged(string propertyName) |
---|
544 | { |
---|
545 | if (PropertyChanged != null) |
---|
546 | PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); |
---|
547 | } |
---|
548 | #endregion |
---|
549 | |
---|
550 | #region Insert / Remove / Replace |
---|
551 | /// <summary> |
---|
552 | /// Inserts text. |
---|
553 | /// </summary> |
---|
554 | /// <param name="offset">The offset at which the text is inserted.</param> |
---|
555 | /// <param name="text">The new text.</param> |
---|
556 | /// <remarks> |
---|
557 | /// Anchors positioned exactly at the insertion offset will move according to their movement type. |
---|
558 | /// For AnchorMovementType.Default, they will move behind the inserted text. |
---|
559 | /// The caret will also move behind the inserted text. |
---|
560 | /// </remarks> |
---|
561 | public void Insert(int offset, string text) |
---|
562 | { |
---|
563 | Replace(offset, 0, new StringTextSource(text), null); |
---|
564 | } |
---|
565 | |
---|
566 | /// <summary> |
---|
567 | /// Inserts text. |
---|
568 | /// </summary> |
---|
569 | /// <param name="offset">The offset at which the text is inserted.</param> |
---|
570 | /// <param name="text">The new text.</param> |
---|
571 | /// <remarks> |
---|
572 | /// Anchors positioned exactly at the insertion offset will move according to their movement type. |
---|
573 | /// For AnchorMovementType.Default, they will move behind the inserted text. |
---|
574 | /// The caret will also move behind the inserted text. |
---|
575 | /// </remarks> |
---|
576 | public void Insert(int offset, ITextSource text) |
---|
577 | { |
---|
578 | Replace(offset, 0, text, null); |
---|
579 | } |
---|
580 | |
---|
581 | /// <summary> |
---|
582 | /// Inserts text. |
---|
583 | /// </summary> |
---|
584 | /// <param name="offset">The offset at which the text is inserted.</param> |
---|
585 | /// <param name="text">The new text.</param> |
---|
586 | /// <param name="defaultAnchorMovementType"> |
---|
587 | /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type. |
---|
588 | /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter. |
---|
589 | /// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter. |
---|
590 | /// </param> |
---|
591 | public void Insert(int offset, string text, AnchorMovementType defaultAnchorMovementType) |
---|
592 | { |
---|
593 | if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) { |
---|
594 | Replace(offset, 0, new StringTextSource(text), OffsetChangeMappingType.KeepAnchorBeforeInsertion); |
---|
595 | } else { |
---|
596 | Replace(offset, 0, new StringTextSource(text), null); |
---|
597 | } |
---|
598 | } |
---|
599 | |
---|
600 | /// <summary> |
---|
601 | /// Inserts text. |
---|
602 | /// </summary> |
---|
603 | /// <param name="offset">The offset at which the text is inserted.</param> |
---|
604 | /// <param name="text">The new text.</param> |
---|
605 | /// <param name="defaultAnchorMovementType"> |
---|
606 | /// Anchors positioned exactly at the insertion offset will move according to the anchor's movement type. |
---|
607 | /// For AnchorMovementType.Default, they will move according to the movement type specified by this parameter. |
---|
608 | /// The caret will also move according to the <paramref name="defaultAnchorMovementType"/> parameter. |
---|
609 | /// </param> |
---|
610 | public void Insert(int offset, ITextSource text, AnchorMovementType defaultAnchorMovementType) |
---|
611 | { |
---|
612 | if (defaultAnchorMovementType == AnchorMovementType.BeforeInsertion) { |
---|
613 | Replace(offset, 0, text, OffsetChangeMappingType.KeepAnchorBeforeInsertion); |
---|
614 | } else { |
---|
615 | Replace(offset, 0, text, null); |
---|
616 | } |
---|
617 | } |
---|
618 | |
---|
619 | /// <summary> |
---|
620 | /// Removes text. |
---|
621 | /// </summary> |
---|
622 | public void Remove(ISegment segment) |
---|
623 | { |
---|
624 | Replace(segment, string.Empty); |
---|
625 | } |
---|
626 | |
---|
627 | /// <summary> |
---|
628 | /// Removes text. |
---|
629 | /// </summary> |
---|
630 | /// <param name="offset">Starting offset of the text to be removed.</param> |
---|
631 | /// <param name="length">Length of the text to be removed.</param> |
---|
632 | public void Remove(int offset, int length) |
---|
633 | { |
---|
634 | Replace(offset, length, StringTextSource.Empty); |
---|
635 | } |
---|
636 | |
---|
637 | internal bool inDocumentChanging; |
---|
638 | |
---|
639 | /// <summary> |
---|
640 | /// Replaces text. |
---|
641 | /// </summary> |
---|
642 | public void Replace(ISegment segment, string text) |
---|
643 | { |
---|
644 | if (segment == null) |
---|
645 | throw new ArgumentNullException("segment"); |
---|
646 | Replace(segment.Offset, segment.Length, new StringTextSource(text), null); |
---|
647 | } |
---|
648 | |
---|
649 | /// <summary> |
---|
650 | /// Replaces text. |
---|
651 | /// </summary> |
---|
652 | public void Replace(ISegment segment, ITextSource text) |
---|
653 | { |
---|
654 | if (segment == null) |
---|
655 | throw new ArgumentNullException("segment"); |
---|
656 | Replace(segment.Offset, segment.Length, text, null); |
---|
657 | } |
---|
658 | |
---|
659 | /// <summary> |
---|
660 | /// Replaces text. |
---|
661 | /// </summary> |
---|
662 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
663 | /// <param name="length">The length of the text to be replaced.</param> |
---|
664 | /// <param name="text">The new text.</param> |
---|
665 | public void Replace(int offset, int length, string text) |
---|
666 | { |
---|
667 | Replace(offset, length, new StringTextSource(text), null); |
---|
668 | } |
---|
669 | |
---|
670 | /// <summary> |
---|
671 | /// Replaces text. |
---|
672 | /// </summary> |
---|
673 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
674 | /// <param name="length">The length of the text to be replaced.</param> |
---|
675 | /// <param name="text">The new text.</param> |
---|
676 | public void Replace(int offset, int length, ITextSource text) |
---|
677 | { |
---|
678 | Replace(offset, length, text, null); |
---|
679 | } |
---|
680 | |
---|
681 | /// <summary> |
---|
682 | /// Replaces text. |
---|
683 | /// </summary> |
---|
684 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
685 | /// <param name="length">The length of the text to be replaced.</param> |
---|
686 | /// <param name="text">The new text.</param> |
---|
687 | /// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text. |
---|
688 | /// This affects how the anchors and segments inside the replaced region behave.</param> |
---|
689 | public void Replace(int offset, int length, string text, OffsetChangeMappingType offsetChangeMappingType) |
---|
690 | { |
---|
691 | Replace(offset, length, new StringTextSource(text), offsetChangeMappingType); |
---|
692 | } |
---|
693 | |
---|
694 | /// <summary> |
---|
695 | /// Replaces text. |
---|
696 | /// </summary> |
---|
697 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
698 | /// <param name="length">The length of the text to be replaced.</param> |
---|
699 | /// <param name="text">The new text.</param> |
---|
700 | /// <param name="offsetChangeMappingType">The offsetChangeMappingType determines how offsets inside the old text are mapped to the new text. |
---|
701 | /// This affects how the anchors and segments inside the replaced region behave.</param> |
---|
702 | public void Replace(int offset, int length, ITextSource text, OffsetChangeMappingType offsetChangeMappingType) |
---|
703 | { |
---|
704 | if (text == null) |
---|
705 | throw new ArgumentNullException("text"); |
---|
706 | // Please see OffsetChangeMappingType XML comments for details on how these modes work. |
---|
707 | switch (offsetChangeMappingType) { |
---|
708 | case OffsetChangeMappingType.Normal: |
---|
709 | Replace(offset, length, text, null); |
---|
710 | break; |
---|
711 | case OffsetChangeMappingType.KeepAnchorBeforeInsertion: |
---|
712 | Replace(offset, length, text, OffsetChangeMap.FromSingleElement( |
---|
713 | new OffsetChangeMapEntry(offset, length, text.TextLength, false, true))); |
---|
714 | break; |
---|
715 | case OffsetChangeMappingType.RemoveAndInsert: |
---|
716 | if (length == 0 || text.TextLength == 0) { |
---|
717 | // only insertion or only removal? |
---|
718 | // OffsetChangeMappingType doesn't matter, just use Normal. |
---|
719 | Replace(offset, length, text, null); |
---|
720 | } else { |
---|
721 | OffsetChangeMap map = new OffsetChangeMap(2); |
---|
722 | map.Add(new OffsetChangeMapEntry(offset, length, 0)); |
---|
723 | map.Add(new OffsetChangeMapEntry(offset, 0, text.TextLength)); |
---|
724 | map.Freeze(); |
---|
725 | Replace(offset, length, text, map); |
---|
726 | } |
---|
727 | break; |
---|
728 | case OffsetChangeMappingType.CharacterReplace: |
---|
729 | if (length == 0 || text.TextLength == 0) { |
---|
730 | // only insertion or only removal? |
---|
731 | // OffsetChangeMappingType doesn't matter, just use Normal. |
---|
732 | Replace(offset, length, text, null); |
---|
733 | } else if (text.TextLength > length) { |
---|
734 | // look at OffsetChangeMappingType.CharacterReplace XML comments on why we need to replace |
---|
735 | // the last character |
---|
736 | OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + length - 1, 1, 1 + text.TextLength - length); |
---|
737 | Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry)); |
---|
738 | } else if (text.TextLength < length) { |
---|
739 | OffsetChangeMapEntry entry = new OffsetChangeMapEntry(offset + text.TextLength, length - text.TextLength, 0, true, false); |
---|
740 | Replace(offset, length, text, OffsetChangeMap.FromSingleElement(entry)); |
---|
741 | } else { |
---|
742 | Replace(offset, length, text, OffsetChangeMap.Empty); |
---|
743 | } |
---|
744 | break; |
---|
745 | default: |
---|
746 | throw new ArgumentOutOfRangeException("offsetChangeMappingType", offsetChangeMappingType, "Invalid enum value"); |
---|
747 | } |
---|
748 | } |
---|
749 | |
---|
750 | /// <summary> |
---|
751 | /// Replaces text. |
---|
752 | /// </summary> |
---|
753 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
754 | /// <param name="length">The length of the text to be replaced.</param> |
---|
755 | /// <param name="text">The new text.</param> |
---|
756 | /// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text. |
---|
757 | /// This affects how the anchors and segments inside the replaced region behave. |
---|
758 | /// If you pass null (the default when using one of the other overloads), the offsets are changed as |
---|
759 | /// in OffsetChangeMappingType.Normal mode. |
---|
760 | /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode). |
---|
761 | /// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>. |
---|
762 | /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting |
---|
763 | /// DocumentChangeEventArgs instance. |
---|
764 | /// </param> |
---|
765 | public void Replace(int offset, int length, string text, OffsetChangeMap offsetChangeMap) |
---|
766 | { |
---|
767 | Replace(offset, length, new StringTextSource(text), offsetChangeMap); |
---|
768 | } |
---|
769 | |
---|
770 | /// <summary> |
---|
771 | /// Replaces text. |
---|
772 | /// </summary> |
---|
773 | /// <param name="offset">The starting offset of the text to be replaced.</param> |
---|
774 | /// <param name="length">The length of the text to be replaced.</param> |
---|
775 | /// <param name="text">The new text.</param> |
---|
776 | /// <param name="offsetChangeMap">The offsetChangeMap determines how offsets inside the old text are mapped to the new text. |
---|
777 | /// This affects how the anchors and segments inside the replaced region behave. |
---|
778 | /// If you pass null (the default when using one of the other overloads), the offsets are changed as |
---|
779 | /// in OffsetChangeMappingType.Normal mode. |
---|
780 | /// If you pass OffsetChangeMap.Empty, then everything will stay in its old place (OffsetChangeMappingType.CharacterReplace mode). |
---|
781 | /// The offsetChangeMap must be a valid 'explanation' for the document change. See <see cref="OffsetChangeMap.IsValidForDocumentChange"/>. |
---|
782 | /// Passing an OffsetChangeMap to the Replace method will automatically freeze it to ensure the thread safety of the resulting |
---|
783 | /// DocumentChangeEventArgs instance. |
---|
784 | /// </param> |
---|
785 | public void Replace(int offset, int length, ITextSource text, OffsetChangeMap offsetChangeMap) |
---|
786 | { |
---|
787 | if (text == null) |
---|
788 | throw new ArgumentNullException("text"); |
---|
789 | text = text.CreateSnapshot(); |
---|
790 | if (offsetChangeMap != null) |
---|
791 | offsetChangeMap.Freeze(); |
---|
792 | |
---|
793 | // Ensure that all changes take place inside an update group. |
---|
794 | // Will also take care of throwing an exception if inDocumentChanging is set. |
---|
795 | BeginUpdate(); |
---|
796 | try { |
---|
797 | // protect document change against corruption by other changes inside the event handlers |
---|
798 | inDocumentChanging = true; |
---|
799 | try { |
---|
800 | // The range verification must wait until after the BeginUpdate() call because the document |
---|
801 | // might be modified inside the UpdateStarted event. |
---|
802 | ThrowIfRangeInvalid(offset, length); |
---|
803 | |
---|
804 | DoReplace(offset, length, text, offsetChangeMap); |
---|
805 | } finally { |
---|
806 | inDocumentChanging = false; |
---|
807 | } |
---|
808 | } finally { |
---|
809 | EndUpdate(); |
---|
810 | } |
---|
811 | } |
---|
812 | |
---|
813 | void DoReplace(int offset, int length, ITextSource newText, OffsetChangeMap offsetChangeMap) |
---|
814 | { |
---|
815 | if (length == 0 && newText.TextLength == 0) |
---|
816 | return; |
---|
817 | |
---|
818 | // trying to replace a single character in 'Normal' mode? |
---|
819 | // for single characters, 'CharacterReplace' mode is equivalent, but more performant |
---|
820 | // (we don't have to touch the anchorTree at all in 'CharacterReplace' mode) |
---|
821 | if (length == 1 && newText.TextLength == 1 && offsetChangeMap == null) |
---|
822 | offsetChangeMap = OffsetChangeMap.Empty; |
---|
823 | |
---|
824 | ITextSource removedText; |
---|
825 | if (length == 0) { |
---|
826 | removedText = StringTextSource.Empty; |
---|
827 | } else if (length < 100) { |
---|
828 | removedText = new StringTextSource(rope.ToString(offset, length)); |
---|
829 | } else { |
---|
830 | // use a rope if the removed string is long |
---|
831 | removedText = new RopeTextSource(rope.GetRange(offset, length)); |
---|
832 | } |
---|
833 | DocumentChangeEventArgs args = new DocumentChangeEventArgs(offset, removedText, newText, offsetChangeMap); |
---|
834 | |
---|
835 | // fire DocumentChanging event |
---|
836 | if (Changing != null) |
---|
837 | Changing(this, args); |
---|
838 | if (textChanging != null) |
---|
839 | textChanging(this, args); |
---|
840 | |
---|
841 | undoStack.Push(this, args); |
---|
842 | |
---|
843 | cachedText = null; // reset cache of complete document text |
---|
844 | fireTextChanged = true; |
---|
845 | DelayedEvents delayedEvents = new DelayedEvents(); |
---|
846 | |
---|
847 | lock (lockObject) { |
---|
848 | // create linked list of checkpoints |
---|
849 | versionProvider.AppendChange(args); |
---|
850 | |
---|
851 | // now update the textBuffer and lineTree |
---|
852 | if (offset == 0 && length == rope.Length) { |
---|
853 | // optimize replacing the whole document |
---|
854 | rope.Clear(); |
---|
855 | var newRopeTextSource = newText as RopeTextSource; |
---|
856 | if (newRopeTextSource != null) |
---|
857 | rope.InsertRange(0, newRopeTextSource.GetRope()); |
---|
858 | else |
---|
859 | rope.InsertText(0, newText.Text); |
---|
860 | lineManager.Rebuild(); |
---|
861 | } else { |
---|
862 | rope.RemoveRange(offset, length); |
---|
863 | lineManager.Remove(offset, length); |
---|
864 | #if DEBUG |
---|
865 | lineTree.CheckProperties(); |
---|
866 | #endif |
---|
867 | var newRopeTextSource = newText as RopeTextSource; |
---|
868 | if (newRopeTextSource != null) |
---|
869 | rope.InsertRange(offset, newRopeTextSource.GetRope()); |
---|
870 | else |
---|
871 | rope.InsertText(offset, newText.Text); |
---|
872 | lineManager.Insert(offset, newText); |
---|
873 | #if DEBUG |
---|
874 | lineTree.CheckProperties(); |
---|
875 | #endif |
---|
876 | } |
---|
877 | } |
---|
878 | |
---|
879 | // update text anchors |
---|
880 | if (offsetChangeMap == null) { |
---|
881 | anchorTree.HandleTextChange(args.CreateSingleChangeMapEntry(), delayedEvents); |
---|
882 | } else { |
---|
883 | foreach (OffsetChangeMapEntry entry in offsetChangeMap) { |
---|
884 | anchorTree.HandleTextChange(entry, delayedEvents); |
---|
885 | } |
---|
886 | } |
---|
887 | |
---|
888 | lineManager.ChangeComplete(args); |
---|
889 | |
---|
890 | // raise delayed events after our data structures are consistent again |
---|
891 | delayedEvents.RaiseEvents(); |
---|
892 | |
---|
893 | // fire DocumentChanged event |
---|
894 | if (Changed != null) |
---|
895 | Changed(this, args); |
---|
896 | if (textChanged != null) |
---|
897 | textChanged(this, args); |
---|
898 | } |
---|
899 | #endregion |
---|
900 | |
---|
901 | #region GetLineBy... |
---|
902 | /// <summary> |
---|
903 | /// Gets a read-only list of lines. |
---|
904 | /// </summary> |
---|
905 | /// <remarks><inheritdoc cref="DocumentLine"/></remarks> |
---|
906 | public IList<DocumentLine> Lines { |
---|
907 | get { return lineTree; } |
---|
908 | } |
---|
909 | |
---|
910 | /// <summary> |
---|
911 | /// Gets a line by the line number: O(log n) |
---|
912 | /// </summary> |
---|
913 | public DocumentLine GetLineByNumber(int number) |
---|
914 | { |
---|
915 | VerifyAccess(); |
---|
916 | if (number < 1 || number > lineTree.LineCount) |
---|
917 | throw new ArgumentOutOfRangeException("number", number, "Value must be between 1 and " + lineTree.LineCount); |
---|
918 | return lineTree.GetByNumber(number); |
---|
919 | } |
---|
920 | |
---|
921 | IDocumentLine IDocument.GetLineByNumber(int lineNumber) |
---|
922 | { |
---|
923 | return GetLineByNumber(lineNumber); |
---|
924 | } |
---|
925 | |
---|
926 | /// <summary> |
---|
927 | /// Gets a document lines by offset. |
---|
928 | /// Runtime: O(log n) |
---|
929 | /// </summary> |
---|
930 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")] |
---|
931 | public DocumentLine GetLineByOffset(int offset) |
---|
932 | { |
---|
933 | VerifyAccess(); |
---|
934 | if (offset < 0 || offset > rope.Length) { |
---|
935 | throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString()); |
---|
936 | } |
---|
937 | return lineTree.GetByOffset(offset); |
---|
938 | } |
---|
939 | |
---|
940 | IDocumentLine IDocument.GetLineByOffset(int offset) |
---|
941 | { |
---|
942 | return GetLineByOffset(offset); |
---|
943 | } |
---|
944 | #endregion |
---|
945 | |
---|
946 | #region GetOffset / GetLocation |
---|
947 | /// <summary> |
---|
948 | /// Gets the offset from a text location. |
---|
949 | /// </summary> |
---|
950 | /// <seealso cref="GetLocation"/> |
---|
951 | public int GetOffset(TextLocation location) |
---|
952 | { |
---|
953 | return GetOffset(location.Line, location.Column); |
---|
954 | } |
---|
955 | |
---|
956 | /// <summary> |
---|
957 | /// Gets the offset from a text location. |
---|
958 | /// </summary> |
---|
959 | /// <seealso cref="GetLocation"/> |
---|
960 | public int GetOffset(int line, int column) |
---|
961 | { |
---|
962 | DocumentLine docLine = GetLineByNumber(line); |
---|
963 | if (column <= 0) |
---|
964 | return docLine.Offset; |
---|
965 | if (column > docLine.Length) |
---|
966 | return docLine.EndOffset; |
---|
967 | return docLine.Offset + column - 1; |
---|
968 | } |
---|
969 | |
---|
970 | /// <summary> |
---|
971 | /// Gets the location from an offset. |
---|
972 | /// </summary> |
---|
973 | /// <seealso cref="GetOffset(TextLocation)"/> |
---|
974 | public TextLocation GetLocation(int offset) |
---|
975 | { |
---|
976 | DocumentLine line = GetLineByOffset(offset); |
---|
977 | return new TextLocation(line.LineNumber, offset - line.Offset + 1); |
---|
978 | } |
---|
979 | #endregion |
---|
980 | |
---|
981 | #region Line Trackers |
---|
982 | readonly ObservableCollection<ILineTracker> lineTrackers = new ObservableCollection<ILineTracker>(); |
---|
983 | |
---|
984 | /// <summary> |
---|
985 | /// Gets the list of <see cref="ILineTracker"/>s attached to this document. |
---|
986 | /// You can add custom line trackers to this list. |
---|
987 | /// </summary> |
---|
988 | public IList<ILineTracker> LineTrackers { |
---|
989 | get { |
---|
990 | VerifyAccess(); |
---|
991 | return lineTrackers; |
---|
992 | } |
---|
993 | } |
---|
994 | #endregion |
---|
995 | |
---|
996 | #region UndoStack |
---|
997 | UndoStack undoStack; |
---|
998 | |
---|
999 | /// <summary> |
---|
1000 | /// Gets the <see cref="UndoStack"/> of the document. |
---|
1001 | /// </summary> |
---|
1002 | /// <remarks>This property can also be used to set the undo stack, e.g. for sharing a common undo stack between multiple documents.</remarks> |
---|
1003 | public UndoStack UndoStack { |
---|
1004 | get { return undoStack; } |
---|
1005 | set { |
---|
1006 | if (value == null) |
---|
1007 | throw new ArgumentNullException(); |
---|
1008 | if (value != undoStack) { |
---|
1009 | undoStack.ClearAll(); // first clear old undo stack, so that it can't be used to perform unexpected changes on this document |
---|
1010 | // ClearAll() will also throw an exception when it's not safe to replace the undo stack (e.g. update is currently in progress) |
---|
1011 | undoStack = value; |
---|
1012 | OnPropertyChanged("UndoStack"); |
---|
1013 | } |
---|
1014 | } |
---|
1015 | } |
---|
1016 | #endregion |
---|
1017 | |
---|
1018 | #region CreateAnchor |
---|
1019 | /// <summary> |
---|
1020 | /// Creates a new <see cref="TextAnchor"/> at the specified offset. |
---|
1021 | /// </summary> |
---|
1022 | /// <inheritdoc cref="TextAnchor" select="remarks|example"/> |
---|
1023 | public TextAnchor CreateAnchor(int offset) |
---|
1024 | { |
---|
1025 | VerifyAccess(); |
---|
1026 | if (offset < 0 || offset > rope.Length) { |
---|
1027 | throw new ArgumentOutOfRangeException("offset", offset, "0 <= offset <= " + rope.Length.ToString(CultureInfo.InvariantCulture)); |
---|
1028 | } |
---|
1029 | return anchorTree.CreateAnchor(offset); |
---|
1030 | } |
---|
1031 | |
---|
1032 | ITextAnchor IDocument.CreateAnchor(int offset) |
---|
1033 | { |
---|
1034 | return CreateAnchor(offset); |
---|
1035 | } |
---|
1036 | #endregion |
---|
1037 | |
---|
1038 | #region LineCount |
---|
1039 | /// <summary> |
---|
1040 | /// Gets the total number of lines in the document. |
---|
1041 | /// Runtime: O(1). |
---|
1042 | /// </summary> |
---|
1043 | public int LineCount { |
---|
1044 | get { |
---|
1045 | VerifyAccess(); |
---|
1046 | return lineTree.LineCount; |
---|
1047 | } |
---|
1048 | } |
---|
1049 | |
---|
1050 | /// <summary> |
---|
1051 | /// Is raised when the LineCount property changes. |
---|
1052 | /// </summary> |
---|
1053 | [Obsolete("This event will be removed in a future version; use the PropertyChanged event instead")] |
---|
1054 | public event EventHandler LineCountChanged; |
---|
1055 | #endregion |
---|
1056 | |
---|
1057 | #region Debugging |
---|
1058 | [Conditional("DEBUG")] |
---|
1059 | internal void DebugVerifyAccess() |
---|
1060 | { |
---|
1061 | VerifyAccess(); |
---|
1062 | } |
---|
1063 | |
---|
1064 | /// <summary> |
---|
1065 | /// Gets the document lines tree in string form. |
---|
1066 | /// </summary> |
---|
1067 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
---|
1068 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] |
---|
1069 | internal string GetLineTreeAsString() |
---|
1070 | { |
---|
1071 | #if DEBUG |
---|
1072 | return lineTree.GetTreeAsString(); |
---|
1073 | #else |
---|
1074 | return "Not available in release build."; |
---|
1075 | #endif |
---|
1076 | } |
---|
1077 | |
---|
1078 | /// <summary> |
---|
1079 | /// Gets the text anchor tree in string form. |
---|
1080 | /// </summary> |
---|
1081 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
---|
1082 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] |
---|
1083 | internal string GetTextAnchorTreeAsString() |
---|
1084 | { |
---|
1085 | #if DEBUG |
---|
1086 | return anchorTree.GetTreeAsString(); |
---|
1087 | #else |
---|
1088 | return "Not available in release build."; |
---|
1089 | #endif |
---|
1090 | } |
---|
1091 | #endregion |
---|
1092 | |
---|
1093 | #region Service Provider |
---|
1094 | IServiceProvider serviceProvider; |
---|
1095 | |
---|
1096 | /// <summary> |
---|
1097 | /// Gets/Sets the service provider associated with this document. |
---|
1098 | /// By default, every TextDocument has its own ServiceContainer; and has the document itself |
---|
1099 | /// registered as <see cref="IDocument"/> and <see cref="TextDocument"/>. |
---|
1100 | /// </summary> |
---|
1101 | public IServiceProvider ServiceProvider { |
---|
1102 | get { |
---|
1103 | VerifyAccess(); |
---|
1104 | if (serviceProvider == null) { |
---|
1105 | var container = new ServiceContainer(); |
---|
1106 | container.AddService(typeof(IDocument), this); |
---|
1107 | container.AddService(typeof(TextDocument), this); |
---|
1108 | serviceProvider = container; |
---|
1109 | } |
---|
1110 | return serviceProvider; |
---|
1111 | } |
---|
1112 | set { |
---|
1113 | VerifyAccess(); |
---|
1114 | if (value == null) |
---|
1115 | throw new ArgumentNullException(); |
---|
1116 | serviceProvider = value; |
---|
1117 | } |
---|
1118 | } |
---|
1119 | |
---|
1120 | object IServiceProvider.GetService(Type serviceType) |
---|
1121 | { |
---|
1122 | return this.ServiceProvider.GetService(serviceType); |
---|
1123 | } |
---|
1124 | #endregion |
---|
1125 | |
---|
1126 | #region FileName |
---|
1127 | string fileName; |
---|
1128 | |
---|
1129 | /// <inheritdoc/> |
---|
1130 | public event EventHandler FileNameChanged; |
---|
1131 | |
---|
1132 | void OnFileNameChanged(EventArgs e) |
---|
1133 | { |
---|
1134 | EventHandler handler = this.FileNameChanged; |
---|
1135 | if (handler != null) |
---|
1136 | handler(this, e); |
---|
1137 | } |
---|
1138 | |
---|
1139 | /// <inheritdoc/> |
---|
1140 | public string FileName { |
---|
1141 | get { return fileName; } |
---|
1142 | set { |
---|
1143 | if (fileName != value) { |
---|
1144 | fileName = value; |
---|
1145 | OnFileNameChanged(EventArgs.Empty); |
---|
1146 | } |
---|
1147 | } |
---|
1148 | } |
---|
1149 | #endregion |
---|
1150 | } |
---|
1151 | } |
---|