Welcome to mirror list, hosted at ThFree Co, Russian Federation.

TextDocument.cs « TextModel « Impl « Text « Editor « src - github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 62d0f5a75f7ea2be14083d20b843459ad578be7e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//  Licensed under the MIT License. See License.txt in the project root for license information.
//
// This file contain implementations details that are subject to change without notice.
// Use at your own risk.
//
namespace Microsoft.VisualStudio.Text.Implementation
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using Microsoft.VisualStudio.Text.Utilities;
    using Microsoft.VisualStudio.Utilities;
    using Microsoft.VisualStudio.Text.Editor;

    internal sealed partial class TextDocument : ITextDocument
    {
        #region Private Members

        private readonly TextDocumentFactoryService _textDocumentFactoryService;
        private ITextBuffer _textBuffer;
        private Encoding _encoding;
        private string _filePath;
        //If the user explicitly chooses the encoding, we want to respect their chosen encoding
        private bool _explicitEncoding;

        //This corresponds to the tools option "Auto-detect UTF-8 encoding"
        //Unfortunately, we cannot dynamically read the option value without adding a dependency on TextLogic which results in a layering violation.
        //Therefore we're going to cache this value on creation of the text document and it will persist the lifetime of the document.
        private bool _attemptUtf8Detection = true;

        private DateTime _lastSavedTimeUtc;
        private DateTime _lastModifiedTimeUtc;
        private int _cleanReiteratedVersion; // The ReiteratedVersionNumber at which the document was last clean (saved, opened, or created)
        private bool _isDirty;
        private bool _isDisposed;
        private bool _raisingDirtyStateChangedEvent;
        private bool _raisingFileActionChangedEvent;
        private bool _reloadingFile;

        #endregion

        #region Construction
        internal TextDocument(ITextBuffer textBuffer, string filePath, DateTime lastModifiedTime, TextDocumentFactoryService textDocumentFactoryService)
            : this(textBuffer, filePath, lastModifiedTime, textDocumentFactoryService, Encoding.UTF8) { }

        internal TextDocument(ITextBuffer textBuffer, string filePath, DateTime lastModifiedTime, TextDocumentFactoryService textDocumentFactoryService, Encoding encoding, bool explicitEncoding = false, bool attemptUtf8Detection = true)
        {
            if (textBuffer == null)
            {
                throw new ArgumentNullException(nameof(textBuffer));
            }
            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }
            if (textDocumentFactoryService == null)
            {
                throw new ArgumentNullException(nameof(textDocumentFactoryService));
            }
            if (encoding == null)
            {
                throw new ArgumentNullException(nameof(encoding));
            }

            _textBuffer = textBuffer;
            _filePath = filePath;
            _lastModifiedTimeUtc = lastModifiedTime;
            _textDocumentFactoryService = textDocumentFactoryService;
            _cleanReiteratedVersion = _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber;
            _isDisposed = false;
            _isDirty = false;
            _reloadingFile = false;
            _raisingDirtyStateChangedEvent = false;
            _raisingFileActionChangedEvent = false;
            _encoding = encoding;
            _explicitEncoding = explicitEncoding;
            _attemptUtf8Detection = attemptUtf8Detection;

            // Keep track of when the text buffer has been changed so that we can update the LastContentModifiedTime
            _textBuffer.ChangedHighPriority += TextBufferChangedHandler;

            _textBuffer.Properties.AddProperty(typeof(ITextDocument), this);
        }
        #endregion

        #region ITextDocument Members

        public string FilePath
        {
            get { return _filePath; }
        }

        public ITextBuffer TextBuffer
        {
            get { return _textBuffer; }
        }

        public bool IsDirty
        {
            get { return _isDirty; }
        }

        public DateTime LastSavedTime
        {
            get { return _lastSavedTimeUtc; }
        }

        public DateTime LastContentModifiedTime
        {
            get { return _lastModifiedTimeUtc; }
        }

        public void Rename(string newFilePath)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException("ITextDocument");
            }
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }
            if (newFilePath == null)
            {
                throw new ArgumentNullException(nameof(newFilePath));
            }

            _filePath = newFilePath;

            RaiseFileActionChangedEvent(_lastModifiedTimeUtc, FileActionTypes.DocumentRenamed, _filePath);
        }

        public ReloadResult Reload()
        {
            return Reload(EditOptions.None);
        }

        private void ReloadBufferFromStream(Stream stream, long fileSize, EditOptions options, Encoding encoding)
        {
            using (var streamReader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, encoding, detectEncodingFromByteOrderMarks: false))
            {
                TextBuffer concreteBuffer = _textBuffer as TextBuffer;
                if (concreteBuffer != null)
                {
                    bool hasConsistentLineEndings;
                    int longestLineLength;
                    StringRebuilder newContent = TextImageLoader.Load(streamReader, fileSize, out hasConsistentLineEndings, out longestLineLength);

                    if (!hasConsistentLineEndings)
                    {
                        // leave a sign that line endings are inconsistent. This is rather nasty but for now
                        // we don't want to pollute the API with this factoid.
                        concreteBuffer.Properties["InconsistentLineEndings"] = true;
                    }
                    else
                    {
                        // this covers a really obscure case where on initial load the file had inconsistent line
                        // endings, but the UI settings were such that it was ignored, and since then the file has
                        // acquired consistent line endings and the UI settings have also changed.
                        concreteBuffer.Properties.RemoveProperty("InconsistentLineEndings");
                    }
                    // leave a similar sign about the longest line in the buffer.
                    concreteBuffer.Properties["LongestLineLength"] = longestLineLength;

                    concreteBuffer.ReloadContent(newContent, options, editTag: this);
                }
                else
                {
                    // we may hit this path if somebody mocks the text buffer in a test.
                    using (var edit = _textBuffer.CreateEdit(options, null, editTag: this))
                    {
                        if (edit.Replace(new Span(0, edit.Snapshot.Length), streamReader.ReadToEnd()))
                        {
                            edit.Apply();
                        }
                        else
                        {
                            edit.Cancel();
                        }
                    }
                }
            }
        }

        public ReloadResult Reload(EditOptions options)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(nameof(ITextDocument));
            }
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }

            Encoding newEncoding;
            var beforeSnapshot = _textBuffer.CurrentSnapshot;
            bool characterSubstitutionsOccurred = false;

            try
            {
                _reloadingFile = true;

                // Load the file and read the contents to the text buffer
                long fileSize;

                using (var stream = TextDocumentFactoryService.OpenFileGuts(_filePath, out _lastModifiedTimeUtc, out fileSize))
                {
                    var detectors = ExtensionSelector.SelectMatchingExtensions(_textDocumentFactoryService.OrderedEncodingDetectors, _textBuffer.ContentType);
                    
                    if(_explicitEncoding)
                    {
                        // If the user explicitly chose their encoding, we want to respect it.
                        newEncoding = this.Encoding;
                    }
                    else
                    {
                        newEncoding = EncodedStreamReader.DetectEncoding(stream, detectors, _textDocumentFactoryService.GuardedOperations);
                    }

                    if (newEncoding == null && _attemptUtf8Detection)
                    {
                        try
                        {
                            var detectorEncoding = new ExtendedCharacterDetector();

                            ReloadBufferFromStream(stream, fileSize, options, detectorEncoding);

                            if (detectorEncoding.DecodedExtendedCharacters)
                            {
                                // Valid UTF-8 but has bytes that are not merely ASCII.
                                newEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
                            }
                            else
                            {
                                // Valid UTF8 but no extended characters, so it looks like valid ASCII.
                                // However, we don't use ASCII here because of the following scenario:
                                // The user with a non-English US system encoding opens a code file that happens to contain ASCII-only contents
                                // Therefore we'll just use their system encoding.
                                newEncoding = Encoding.Default;
                            }
                        }
                        catch (DecoderFallbackException)
                        {
                            // Not valid UTF-8.
                            // Proceed to the next if block to try the system's default codepage.
                            // For example, this occurs when you have extended characters like € in a UTF-8 file or ANSI file.
                            // We reset the stream so we can continue loading with the default system encoding.
                            Debug.Assert(newEncoding == null);
                            Debug.Assert(beforeSnapshot.Version.Next == null);
                            stream.Position = 0;
                        }
                    }

                    // If all else didn't work, use system's default encoding.
                    if (newEncoding == null)
                    {
                        newEncoding = Encoding.Default;
                    }

                    //If there is no "Next" version of the original snapshot, we have not successfully reloaded the document
                    if(beforeSnapshot.Version.Next == null)
                    {
                        //We use this fall back detector to observe whether or not character substitutions 
                        //occur while we're reading the stream
                        var fallbackDetector = new FallbackDetector(newEncoding.DecoderFallback);
                        var modifiedEncoding = (Encoding)newEncoding.Clone();
                        modifiedEncoding.DecoderFallback = fallbackDetector;

                        Debug.Assert(stream.Position == 0);
                        ReloadBufferFromStream(stream, fileSize, options, modifiedEncoding);

                        if(fallbackDetector.FallbackOccurred)
                        {
                            characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred;
                        }
                    }
                }
            }
            finally
            {
                _reloadingFile = false;
            }

            //The snapshot on a reload will change even if the contents of the before & after files are identical (differences will simply find an
            //empty set of changes) so this test is a measure of whether of not the reload succeeded.
            if (beforeSnapshot.Version.Next != null)
            {
                // Update status
                // set the "clean" reiterated version number to the reiterated version number of the version immediately
                // after the before snapshot (which is the state of the buffer after loading the document but before any
                // subsequent edits made in the text buffer changed events).
                _cleanReiteratedVersion = beforeSnapshot.Version.Next.ReiteratedVersionNumber;

                // TODO: the following event really should be queued up through the buffer group so that it comes before
                // the text changed event (and any subsequent text changed event invoked from an event handler)
                RaiseFileActionChangedEvent(_lastModifiedTimeUtc, FileActionTypes.ContentLoadedFromDisk, _filePath);
                this.Encoding = newEncoding;
                return characterSubstitutionsOccurred ? ReloadResult.SucceededWithCharacterSubstitutions : ReloadResult.Succeeded;
            }
            else
            {
                return ReloadResult.Aborted;
            }
        }

        public bool IsReloading
        {
            get { return _reloadingFile; }
        }

        public void Save()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException("ITextDocument");
            }
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }

            // Before saving the document check if we need to change the encoding of the file as per codingconventions of the repo.
            if (_textBuffer.Properties.TryGetProperty<Encoding>("EncodingToBeAppliedOnSave", out Encoding encodingToBeAppliedOnSave))
            {
                this.Encoding = encodingToBeAppliedOnSave;
                _textBuffer.Properties.RemoveProperty("EncodingToBeAppliedOnSave");
            }

            PerformSave(FileMode.Create, _filePath, false);
            UpdateSaveStatus(_filePath, false);
        }

        public void SaveAs(string filePath, bool overwrite)
        {
            SaveAs(filePath, overwrite, false);
        }

        public void SaveAs(string filePath, bool overwrite, IContentType newContentType)
        {
            SaveAs(filePath, overwrite, false, newContentType);
        }

        public void SaveCopy(string filePath, bool overwrite)
        {
            SaveCopy(filePath, overwrite, false);
        }

        public void SaveAs(string filePath, bool overwrite, bool createFolder)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException("ITextDocument");
            }
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }
            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder);
            UpdateSaveStatus(filePath, !string.Equals(_filePath, filePath, StringComparison.Ordinal));

            // file path won't be updated if the save fails (in which case PerformSave will throw an exception)

            _filePath = filePath;
        }

        public void SaveAs(string filePath, bool overwrite, bool createFolder, IContentType newContentType)
        {
             if (newContentType == null)
            {
                throw new ArgumentNullException(nameof(newContentType));
            }
             SaveAs(filePath, overwrite, createFolder);
            // content type won't be changed if the save fails (in which case SaveAs will throw an exception)
            _textBuffer.ChangeContentType(newContentType, null);
        }

        public void SaveCopy(string filePath, bool overwrite, bool createFolder)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException("ITextDocument");
            }
            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder);
            // Don't update save status
        }

        private void PerformSave(FileMode fileMode, string filePath, bool createFolder)
        {
            // check whether directory of the path exists
            if (createFolder)
            {
                string fileDirectoryName = Path.GetDirectoryName(filePath);
                if (!string.IsNullOrEmpty(fileDirectoryName) && !Directory.Exists(fileDirectoryName))
                {
                    Directory.CreateDirectory(fileDirectoryName);
                }
            }
            FileUtilities.SaveSnapshot(_textBuffer.CurrentSnapshot, fileMode, _encoding, filePath);
        }

        private void UpdateSaveStatus(string filePath, bool renamed)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            _lastSavedTimeUtc = fileInfo.LastWriteTimeUtc;
            _cleanReiteratedVersion = _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber;

            FileActionTypes actionType = FileActionTypes.ContentSavedToDisk;
            if (renamed)
            {
                actionType |= FileActionTypes.DocumentRenamed;
            }
            RaiseFileActionChangedEvent(_lastSavedTimeUtc, actionType, filePath);
        }

        public void UpdateDirtyState(bool isDirtied, DateTime lastContentModifiedTimeUtc)
        {
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }

            if (_isDisposed)
            {
                throw new ObjectDisposedException("ITextDocument");
            }

            _lastModifiedTimeUtc = lastContentModifiedTimeUtc;

            RaiseDirtyStateChangedEvent(isDirtied);
        }

        public Encoding Encoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }

                Encoding oldEncoding = _encoding;

                _encoding = value;
                
                if (!_encoding.Equals(oldEncoding))
                {
                    _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, EncodingChanged, new EncodingChangedEventArgs(oldEncoding, _encoding));
                }
            }
        }

        public void SetEncoderFallback(EncoderFallback fallback)
        {
            _encoding = Encoding.GetEncoding(_encoding.CodePage, fallback, _encoding.DecoderFallback);
            // no event here!
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent)
            {
                throw new InvalidOperationException();
            }

            if (!_isDisposed)
            {
                _textBuffer.ChangedHighPriority -= TextBufferChangedHandler;
                _textBuffer.Properties.RemoveProperty(typeof(ITextDocument));

                _isDisposed = true;

                _textDocumentFactoryService.RaiseTextDocumentDisposed(this);
                GC.SuppressFinalize(this);

                _textBuffer = null; // why?
            }
        }

        #endregion

        #region Private helpers

        private void TextBufferChangedHandler(object sender, TextContentChangedEventArgs e)
        {
            // We don't want to process textbuffer changes that were caused by ourselves
            if (e.EditTag != this)
            {
                _lastModifiedTimeUtc = DateTime.UtcNow;

                // If the edit was the result of an undo/redo action that took us back to the clean ReiteratedVersionNumber,
                // the document is no longer dirty
                if (e.AfterVersion.ReiteratedVersionNumber == _cleanReiteratedVersion)
                {
                    RaiseDirtyStateChangedEvent(false);
                }
                else
                {
                    RaiseDirtyStateChangedEvent(true);
                }
            }
        }

        /// <summary>
        /// Raises events for when the dirty state changes
        /// </summary>
        private void RaiseDirtyStateChangedEvent(bool newDirtyState)
        {
            _raisingDirtyStateChangedEvent = true;

            try
            {
                bool dirtyStateChanged = (_isDirty != newDirtyState);

                if (dirtyStateChanged)
                {
                    _isDirty = newDirtyState;

                    _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, DirtyStateChanged);
                }
            }
            finally
            {
                _raisingDirtyStateChangedEvent = false;
            }
        }

        /// <summary>
        /// Raises events for when a file load/save occurs.
        /// </summary>
        private void RaiseFileActionChangedEvent(DateTime actionTime, FileActionTypes actionType, string filePath)
        {
            _raisingFileActionChangedEvent = true;

            try
            {
                if ((actionType & FileActionTypes.ContentLoadedFromDisk) == FileActionTypes.ContentLoadedFromDisk ||
                    (actionType & FileActionTypes.ContentSavedToDisk) == FileActionTypes.ContentSavedToDisk)
                {
                    // We did a reload or a save so we probably want to clear the dirty flag unless someone modified
                    // the buffer -- changing the reiterated version number -- between the reload and when this call was made
                    // (for example, modifying the buffer in the text buffer changed event).
                    if (_cleanReiteratedVersion == _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber)
                    {
                        RaiseDirtyStateChangedEvent(false);
                    }
                }

               _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, FileActionOccurred, new TextDocumentFileActionEventArgs(filePath, actionTime, actionType));

            }
            finally
            {
               _raisingFileActionChangedEvent = false;
            }
        }

        #endregion

        public event EventHandler<TextDocumentFileActionEventArgs> FileActionOccurred;
        public event EventHandler DirtyStateChanged;
        public event EventHandler<EncodingChangedEventArgs> EncodingChanged;

        /// <summary>
        /// An accessor for isDisposed to be used by <see cref="TextDocumentFactoryService"/>.
        /// </summary>
        internal bool IsDisposed
        {
            get { return _isDisposed; }
        }
    }
}