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

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirill Osenkov <KirillOsenkov@users.noreply.github.com>2017-10-10 23:56:02 +0300
committerGitHub <noreply@github.com>2017-10-10 23:56:02 +0300
commit745eea2fa0c121b43f4d0f4148e290e8310e86ef (patch)
tree5c95f83e5f4126112bb58d64961aa658f8d8d99c
parent9859bbb12de1f629927f4109e9105616b7d8f3f8 (diff)
Initial commit of the non-WPF implementation layer. (#7)
-rw-r--r--Directory.build.props14
-rw-r--r--Editor.sln48
-rw-r--r--NuGet.config7
-rw-r--r--src/Core/Def/BaseUtility/IGuardedOperations.cs181
-rw-r--r--src/Core/Def/ContentType/IContentTypeMetadata.cs19
-rw-r--r--src/Core/Def/ContentType/IContentTypeRegistryService2.cs24
-rw-r--r--src/Core/Def/ContentType/INameAndReplacesMetadata.cs27
-rw-r--r--src/Core/Def/ContentType/INamedContentTypeMetadata.cs13
-rw-r--r--src/Core/Def/ContentType/MimeTypeAttribute.cs41
-rw-r--r--src/Core/Def/CoreUtility.csproj74
-rw-r--r--src/Core/Impl/ContentType/ContentTypeImpl.cs188
-rw-r--r--src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs789
-rw-r--r--src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs19
-rw-r--r--src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs19
-rw-r--r--src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs24
-rw-r--r--src/Core/Impl/ContentType/StringToContentTypesMap.cs129
-rw-r--r--src/Core/Impl/ContentType/Strings.Designer.cs153
-rw-r--r--src/Core/Impl/ContentType/Strings.resx150
-rw-r--r--src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs364
-rw-r--r--src/Language/Impl/StandardClassification/StandardClassificationService.cs335
-rw-r--r--src/Language/Impl/StandardClassification/Strings.Designer.cs270
-rw-r--r--src/Language/Impl/StandardClassification/Strings.resx212
-rw-r--r--src/Microsoft.VisualStudio.Text.Implementation.csproj32
-rw-r--r--src/Text/Def/Internal/TextData/ExtensionMethods.cs74
-rw-r--r--src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs22
-rw-r--r--src/Text/Def/Internal/TextData/IObjectTracker.cs23
-rw-r--r--src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs60
-rw-r--r--src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs35
-rw-r--r--src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs58
-rw-r--r--src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs36
-rw-r--r--src/Text/Def/Internal/TextData/JoinableTaskHelper.cs80
-rw-r--r--src/Text/Def/Internal/TextData/LazyObservableCollection.cs680
-rw-r--r--src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs95
-rw-r--r--src/Text/Def/Internal/TextData/TrackingSpanTree.cs618
-rw-r--r--src/Text/Def/Internal/TextData/UnicodeWordExtent.cs691
-rw-r--r--src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs23
-rw-r--r--src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs34
-rw-r--r--src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs76
-rw-r--r--src/Text/Def/Internal/TextLogic/IAccurateTagger.cs33
-rw-r--r--src/Text/Def/Internal/TextLogic/IElisionTag.cs26
-rw-r--r--src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs89
-rw-r--r--src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs23
-rw-r--r--src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs29
-rw-r--r--src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs24
-rw-r--r--src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs21
-rw-r--r--src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs101
-rw-r--r--src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs39
-rw-r--r--src/Text/Def/Internal/TextLogic/PriorityAttribute.cs35
-rw-r--r--src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs59
-rw-r--r--src/Text/Def/Internal/TextLogic/TelemetryEventType.cs33
-rw-r--r--src/Text/Def/Internal/TextLogic/TelemetryResult.cs35
-rw-r--r--src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs38
-rw-r--r--src/Text/Def/Internal/TextUI/Caret.cs237
-rw-r--r--src/Text/Def/Internal/TextUI/DisplayTextPoint.cs144
-rw-r--r--src/Text/Def/Internal/TextUI/DisplayTextRange.cs93
-rw-r--r--src/Text/Def/Internal/TextUI/HowToShow.cs31
-rw-r--r--src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs41
-rw-r--r--src/Text/Def/Internal/TextUI/IAnnotationTag.cs56
-rw-r--r--src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs93
-rw-r--r--src/Text/Def/Internal/TextUI/IBufferPrimitives.cs20
-rw-r--r--src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs55
-rw-r--r--src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs33
-rw-r--r--src/Text/Def/Internal/TextUI/IMapEditToData.cs16
-rw-r--r--src/Text/Def/Internal/TextUI/IObscuringTip.cs36
-rw-r--r--src/Text/Def/Internal/TextUI/IObscuringTipManager.cs19
-rw-r--r--src/Text/Def/Internal/TextUI/IStructureTipManager.cs25
-rw-r--r--src/Text/Def/Internal/TextUI/ITextView2.cs46
-rw-r--r--src/Text/Def/Internal/TextUI/IThumbnailSupport.cs20
-rw-r--r--src/Text/Def/Internal/TextUI/IViewPrimitives.cs57
-rw-r--r--src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs78
-rw-r--r--src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs17
-rw-r--r--src/Text/Def/Internal/TextUI/Selection.cs76
-rw-r--r--src/Text/Def/Internal/TextUI/TextBuffer.cs103
-rw-r--r--src/Text/Def/Internal/TextUI/TextPoint.cs357
-rw-r--r--src/Text/Def/Internal/TextUI/TextRange.cs257
-rw-r--r--src/Text/Def/Internal/TextUI/TextView.cs165
-rw-r--r--src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs16
-rw-r--r--src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs45
-rw-r--r--src/Text/Def/TextData/Model/ITextBuffer2.cs35
-rw-r--r--src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs71
-rw-r--r--src/Text/Def/TextData/Model/ITextImage.cs174
-rw-r--r--src/Text/Def/TextData/Model/ITextImageFactoryService.cs35
-rw-r--r--src/Text/Def/TextData/Model/ITextImageVersion.cs70
-rw-r--r--src/Text/Def/TextData/Model/ITextSnapshot2.cs32
-rw-r--r--src/Text/Def/TextData/Model/ITextVersion2.cs18
-rw-r--r--src/Text/Def/TextData/Model/TextImageLine.cs147
-rw-r--r--src/Text/Def/TextData/Model/VersionedPosition.cs90
-rw-r--r--src/Text/Def/TextData/Model/VersionedSpan.cs90
-rw-r--r--src/Text/Def/TextUI/Adornments/BlockContext.cs62
-rw-r--r--src/Text/Def/TextUI/Adornments/IBlockContext.cs26
-rw-r--r--src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs23
-rw-r--r--src/Text/Def/TextUI/Adornments/IBlockContextSource.cs23
-rw-r--r--src/Text/Def/TextUI/Adornments/IStructureContextSource.cs32
-rw-r--r--src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs29
-rw-r--r--src/Text/Def/TextUI/Adornments/IStructureElement.cs84
-rw-r--r--src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs73
-rw-r--r--src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs40
-rw-r--r--src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs26
-rw-r--r--src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs34
-rw-r--r--src/Text/Def/TextUI/Editor/ConnectionReason.cs27
-rw-r--r--src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs47
-rw-r--r--src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs23
-rw-r--r--src/Text/Def/TextUI/Tags/BlockTag.cs98
-rw-r--r--src/Text/Def/TextUI/Tags/IBlockTag.cs84
-rw-r--r--src/Text/Def/TextUI/Tags/IStructureTag.cs119
-rw-r--r--src/Text/Def/TextUI/Tags/StructureTag.cs207
-rw-r--r--src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs370
-rw-r--r--src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs42
-rw-r--r--src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs98
-rw-r--r--src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs43
-rw-r--r--src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs126
-rw-r--r--src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs65
-rw-r--r--src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs253
-rw-r--r--src/Text/Impl/ClassificationType/Strings.Designer.cs81
-rw-r--r--src/Text/Impl/ClassificationType/Strings.resx126
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs136
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs153
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs54
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs161
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs83
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs74
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs214
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs974
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs801
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs45
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs270
-rw-r--r--src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs132
-rw-r--r--src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs261
-rw-r--r--src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs298
-rw-r--r--src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs235
-rw-r--r--src/Text/Impl/EditorOperations/EditorOperations.cs4590
-rw-r--r--src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs89
-rw-r--r--src/Text/Impl/EditorOperations/Strings.Designer.cs486
-rw-r--r--src/Text/Impl/EditorOperations/Strings.resx261
-rw-r--r--src/Text/Impl/EditorOperations/TextEditAction.cs25
-rw-r--r--src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs140
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptions.cs360
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs185
-rw-r--r--src/Text/Impl/EditorOptions/TextModelEditorOptions.cs139
-rw-r--r--src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs29
-rw-r--r--src/Text/Impl/EditorPrimitives/BufferPrimitives.cs30
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs134
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs61
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs978
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs499
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs225
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs496
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs1015
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs380
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs157
-rw-r--r--src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs72
-rw-r--r--src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs39
-rw-r--r--src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs81
-rw-r--r--src/Text/Impl/EditorPrimitives/Strings.Designer.cs99
-rw-r--r--src/Text/Impl/EditorPrimitives/Strings.resx132
-rw-r--r--src/Text/Impl/EditorPrimitives/ViewPrimitives.cs52
-rw-r--r--src/Text/Impl/Navigation/DefaultTextNavigator.cs202
-rw-r--r--src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs98
-rw-r--r--src/Text/Impl/Outlining/Collapsible.cs105
-rw-r--r--src/Text/Impl/Outlining/Outlining.cd35
-rw-r--r--src/Text/Impl/Outlining/OutliningManager.cs741
-rw-r--r--src/Text/Impl/Outlining/OutliningManagerService.cs45
-rw-r--r--src/Text/Impl/StandaloneUndo/AutoEnclose.cs29
-rw-r--r--src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs38
-rw-r--r--src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs128
-rw-r--r--src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs32
-rw-r--r--src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs60
-rw-r--r--src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs546
-rw-r--r--src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs273
-rw-r--r--src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs397
-rw-r--r--src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs16
-rw-r--r--src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs105
-rw-r--r--src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs25
-rw-r--r--src/Text/Impl/TagAggregator/TagAggregator.cs708
-rw-r--r--src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs168
-rw-r--r--src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs117
-rw-r--r--src/Text/Impl/TextBufferUndoManager/Strings.resx138
-rw-r--r--src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs287
-rw-r--r--src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs204
-rw-r--r--src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs75
-rw-r--r--src/Text/Impl/TextModel/BaseBuffer.cs1120
-rw-r--r--src/Text/Impl/TextModel/BaseSnapshot.cs203
-rw-r--r--src/Text/Impl/TextModel/BufferFactoryService.cs409
-rw-r--r--src/Text/Impl/TextModel/BufferGroup.cs688
-rw-r--r--src/Text/Impl/TextModel/CachingTextImage.cs102
-rw-r--r--src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd107
-rw-r--r--src/Text/Impl/TextModel/EncodedStreamReader.cs134
-rw-r--r--src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs51
-rw-r--r--src/Text/Impl/TextModel/ExtendedCharacterDetector.cs37
-rw-r--r--src/Text/Impl/TextModel/FallbackDetector.cs76
-rw-r--r--src/Text/Impl/TextModel/FileUtilities.cs257
-rw-r--r--src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs38
-rw-r--r--src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs97
-rw-r--r--src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs112
-rw-r--r--src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs425
-rw-r--r--src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs190
-rw-r--r--src/Text/Impl/TextModel/IInternalTextBufferFactory.cs25
-rw-r--r--src/Text/Impl/TextModel/ISubordinateTextEdit.cs56
-rw-r--r--src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs33
-rw-r--r--src/Text/Impl/TextModel/MappingPoint.cs153
-rw-r--r--src/Text/Impl/TextModel/MappingSpan.cs162
-rw-r--r--src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs523
-rw-r--r--src/Text/Impl/TextModel/PersistentSpan.cs216
-rw-r--r--src/Text/Impl/TextModel/PersistentSpanFactory.cs295
-rw-r--r--src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs228
-rw-r--r--src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs73
-rw-r--r--src/Text/Impl/TextModel/Projection/BufferGraph.cs773
-rw-r--r--src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs29
-rw-r--r--src/Text/Impl/TextModel/Projection/ElisionBuffer.cs489
-rw-r--r--src/Text/Impl/TextModel/Projection/ElisionMap.cs330
-rw-r--r--src/Text/Impl/TextModel/Projection/ElisionMapNode.cs1184
-rw-r--r--src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs262
-rw-r--r--src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs1914
-rw-r--r--src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs757
-rw-r--r--src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs86
-rw-r--r--src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs63
-rw-r--r--src/Text/Impl/TextModel/Projection/WeakEventHook.cs101
-rw-r--r--src/Text/Impl/TextModel/ReadOnlyRegion.cs91
-rw-r--r--src/Text/Impl/TextModel/ReadOnlySpan.cs147
-rw-r--r--src/Text/Impl/TextModel/ReadOnlySpanCollection.cs385
-rw-r--r--src/Text/Impl/TextModel/Storage/CharStream.cs133
-rw-r--r--src/Text/Impl/TextModel/Storage/Compressor.cs77
-rw-r--r--src/Text/Impl/TextModel/Storage/ILineBreaks.cs35
-rw-r--r--src/Text/Impl/TextModel/Storage/LineBreakManager.cs142
-rw-r--r--src/Text/Impl/TextModel/Storage/Page.cs48
-rw-r--r--src/Text/Impl/TextModel/Storage/PageManager.cs66
-rw-r--r--src/Text/Impl/TextModel/Storage/TextImageLoader.cs199
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs378
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs442
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs90
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs82
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs115
-rw-r--r--src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs186
-rw-r--r--src/Text/Impl/TextModel/Strings.Designer.cs270
-rw-r--r--src/Text/Impl/TextModel/Strings.resx189
-rw-r--r--src/Text/Impl/TextModel/TextBuffer.cs287
-rw-r--r--src/Text/Impl/TextModel/TextChange.cs351
-rw-r--r--src/Text/Impl/TextModel/TextDocument.cs593
-rw-r--r--src/Text/Impl/TextModel/TextDocumentFactoryService.cs340
-rw-r--r--src/Text/Impl/TextModel/TextImageVersion.cs127
-rw-r--r--src/Text/Impl/TextModel/TextModelUtilities.cs61
-rw-r--r--src/Text/Impl/TextModel/TextSnapshot.cs33
-rw-r--r--src/Text/Impl/TextModel/TextSnapshotLine.cs164
-rw-r--r--src/Text/Impl/TextModel/TextVersion.cs189
-rw-r--r--src/Text/Impl/TextModel/TrackingPoint.cs101
-rw-r--r--src/Text/Impl/TextModel/TrackingSpan.cs125
-rw-r--r--src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs236
-rw-r--r--src/Text/Impl/TextModel/VersionNumberPosition.cs34
-rw-r--r--src/Text/Impl/TextSearch/BackgroundSearch.cs450
-rw-r--r--src/Text/Impl/TextSearch/TextSearchNavigator.cs521
-rw-r--r--src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs37
-rw-r--r--src/Text/Impl/TextSearch/TextSearchService.cs751
-rw-r--r--src/Text/Impl/TextSearch/TextSearchTagger.cs281
-rw-r--r--src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs37
-rw-r--r--src/Text/Util/TextDataUtil/ArgumentValidation.cs23
-rw-r--r--src/Text/Util/TextDataUtil/BufferTracker.cs151
-rw-r--r--src/Text/Util/TextDataUtil/DifferenceCollection.cs201
-rw-r--r--src/Text/Util/TextDataUtil/ExtensionSelector.cs71
-rw-r--r--src/Text/Util/TextDataUtil/FrugalList.cs416
-rw-r--r--src/Text/Util/TextDataUtil/GuardedOperations.cs675
-rw-r--r--src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs15
-rw-r--r--src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs15
-rw-r--r--src/Text/Util/TextDataUtil/ListUtilities.cs64
-rw-r--r--src/Text/Util/TextDataUtil/MappingHelper.cs275
-rw-r--r--src/Text/Util/TextDataUtil/MappingPointSnapshot.cs169
-rw-r--r--src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs209
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs55
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs527
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs279
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs51
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs43
-rw-r--r--src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs93
-rw-r--r--src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs287
-rw-r--r--src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs43
-rw-r--r--src/Text/Util/TextDataUtil/SnapshotTracker.cs54
-rw-r--r--src/Text/Util/TextDataUtil/TextModelOptions.cs22
-rw-r--r--src/Text/Util/TextDataUtil/TextUtilities.cs438
-rw-r--r--src/Text/Util/TextDataUtil/WeakReferenceForKey.cs102
-rw-r--r--src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs19
-rw-r--r--src/Text/Util/TextLogicUtil/ITaggerMetadata.cs23
-rw-r--r--src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs51
-rw-r--r--src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs15
-rw-r--r--src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs20
-rw-r--r--src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs13
-rw-r--r--src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs40
-rw-r--r--src/Text/Util/TextUIUtil/UIExtensionSelector.cs83
-rw-r--r--src/Text/Util/TextUIUtil/VacuousTextViewModel.cs77
-rw-r--r--src/key.snkbin0 -> 596 bytes
289 files changed, 54480 insertions, 448 deletions
diff --git a/Directory.build.props b/Directory.build.props
new file mode 100644
index 0000000..9ad5ef8
--- /dev/null
+++ b/Directory.build.props
@@ -0,0 +1,14 @@
+<Project>
+ <PropertyGroup>
+ <RootDirectory>$(MSBuildThisFileDirectory)</RootDirectory>
+ <OutDir>$(RootDirectory)\bin\$(Configuration)\$(MSBuildProjectName)\</OutDir>
+ <BaseIntermediateOutputPath>$(RootDirectory)\obj\$(Configuration)\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
+ <IntermediateOutputPath>$(RootDirectory)\obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <MicrosoftVisualStudioThreadingVersion>15.3.23</MicrosoftVisualStudioThreadingVersion>
+ <MicrosoftVisualStudioUtilitiesVersion>15.0.26606</MicrosoftVisualStudioUtilitiesVersion>
+ <MicrosoftVisualStudioValidationVersion>15.3.15</MicrosoftVisualStudioValidationVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/Editor.sln b/Editor.sln
new file mode 100644
index 0000000..0a4f81e
--- /dev/null
+++ b/Editor.sln
@@ -0,0 +1,48 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Text.Implementation", "src\Microsoft.VisualStudio.Text.Implementation.csproj", "{BE7BC037-1934-4C01-8CC6-3D041F07AF2B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreUtility", "src\Core\Def\CoreUtility.csproj", "{0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextData", "src\Text\Def\TextData\TextData.csproj", "{DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextLogic", "src\Text\Def\TextLogic\TextLogic.csproj", "{593E6B4A-038E-4126-9051-4C72DCA1D52E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextUI", "src\Text\Def\TextUI\TextUI.csproj", "{E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {02CA45CA-E68B-4F34-891E-09831EEE4E54}
+ EndGlobalSection
+EndGlobal
diff --git a/NuGet.config b/NuGet.config
new file mode 100644
index 0000000..9023385
--- /dev/null
+++ b/NuGet.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <packageSources>
+ <add key="Nuget Official" value ="https://api.nuget.org/v3/index.json" />
+ <add key="VS Editor" value="https://www.myget.org/F/vs-editor/" />
+ </packageSources>
+</configuration>
diff --git a/src/Core/Def/BaseUtility/IGuardedOperations.cs b/src/Core/Def/BaseUtility/IGuardedOperations.cs
new file mode 100644
index 0000000..839018f
--- /dev/null
+++ b/src/Core/Def/BaseUtility/IGuardedOperations.cs
@@ -0,0 +1,181 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Operations that guard calls to extensions code and log errors.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ public interface IGuardedOperations
+ {
+ /// <summary>
+ /// Makes a guarded call to an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ void CallExtensionPoint(Action call);
+
+ /// <summary>
+ /// Makes a guarded call to an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ void CallExtensionPoint(object errorSource, Action call);
+
+ /// <summary>
+ /// Makes a guarded call to an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ T CallExtensionPoint<T>(Func<T> call, T valueOnThrow);
+
+ /// <summary>
+ /// Makes a guarded call to an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow);
+
+ /// <summary>
+ /// Makes a guarded call to an async extension point.
+ /// </summary>
+ /// <param name="asyncAction">The extension point to be called.</param>
+ /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ Task CallExtensionPointAsync(Func<Task> asyncAction);
+
+ /// <summary>
+ /// Makes a guarded call to an async extension point.
+ /// </summary>
+ /// <param name="asyncAction">The extension point to be called.</param>
+ /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ Task CallExtensionPointAsync(object errorSource, Func<Task> asyncAction);
+
+ /// <summary>
+ /// Makes a guarded call to an async extension point.
+ /// </summary>
+ /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam>
+ /// <param name="asyncCall">The extension point to be called.</param>
+ /// <param name="valueOnThrow">The value returned if call failed.</param>
+ /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow);
+
+ /// <summary>
+ /// Makes a guarded call to an async extension point.
+ /// </summary>
+ /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam>
+ /// <param name="asyncCall">The extension point to be called.</param>
+ /// <param name="valueOnThrow">The value returned if call failed.</param>
+ /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow);
+
+ /// <summary>
+ /// Selects eligible extension factories.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ IEnumerable<Lazy<TExtensionFactory, TMetadataView>> FindEligibleFactories<TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService)
+ where TExtensionFactory : class
+ where TMetadataView : INamedContentTypeMetadata;
+
+ /// <summary>
+ /// Handles an exception occured in a call to an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ void HandleException(object errorSource, Exception e);
+
+ /// <summary>
+ /// Safely instantiates an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ TExtension InstantiateExtension<TExtension>(object errorSource, Lazy<TExtension> provider);
+
+ /// <summary>
+ /// Safely instantiates an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ TExtension InstantiateExtension<TExtension, TMetadata>(object errorSource, Lazy<TExtension, TMetadata> provider);
+
+ /// <summary>
+ /// Safely instantiates an extension point.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ TExtensionInstance InstantiateExtension<TExtension, TMetadata, TExtensionInstance>(object errorSource, Lazy<TExtension, TMetadata> provider, Func<TExtension, TExtensionInstance> getter);
+
+ /// <summary>
+ /// Safely invokes best matching extension factory.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ TExtension InvokeBestMatchingFactory<TExtension, TMetadataView>(IList<Lazy<TExtension, TMetadataView>> providerHandles, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource) where TMetadataView : IContentTypeMetadata;
+
+ /// <summary>
+ /// Safely invokes best matching extension factory.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ TExtensionInstance InvokeBestMatchingFactory<TExtensionFactory, TExtensionInstance, TMetadataView>(IList<Lazy<TExtensionFactory, TMetadataView>> providerHandles, IContentType dataContentType, Func<TExtensionFactory, TExtensionInstance> getter, IContentTypeRegistryService contentTypeRegistryService, object errorSource)
+ where TExtensionFactory : class
+ where TMetadataView : IContentTypeMetadata;
+
+ /// <summary>
+ /// Safely invokes all eligible extension factories.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ List<TExtensionInstance> InvokeEligibleFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource)
+ where TExtensionInstance : class
+ where TExtensionFactory : class
+ where TMetadataView : INamedContentTypeMetadata;
+
+ /// <summary>
+ /// Safely invokes all matching extension factories.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, object errorSource)
+ where TExtensionInstance : class
+ where TExtensionFactory : class
+ where TMetadataView : IContentTypeMetadata;
+
+ /// <summary>
+ /// Safely raises an event.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ void RaiseEvent(object sender, EventHandler eventHandlers);
+
+ /// <summary>
+ /// Safely raises an event.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs;
+
+ /// <summary>
+ /// Safely raises an event on a background thread.
+ /// </summary>
+ /// <remarks>This class supports the Visual Studio
+ /// infrastructure and in general is not intended to be used directly from your code.</remarks>
+ Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs;
+ }
+}
diff --git a/src/Core/Def/ContentType/IContentTypeMetadata.cs b/src/Core/Def/ContentType/IContentTypeMetadata.cs
new file mode 100644
index 0000000..aeb5860
--- /dev/null
+++ b/src/Core/Def/ContentType/IContentTypeMetadata.cs
@@ -0,0 +1,19 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Represents MEF metadata view corresponding to the <see cref="ContentTypeAttribute"/>s.
+ /// </summary>
+ public interface IContentTypeMetadata
+ {
+ /// <summary>
+ /// List of declared content types.
+ /// </summary>
+ IEnumerable<string> ContentTypes { get; }
+ }
+}
diff --git a/src/Core/Def/ContentType/IContentTypeRegistryService2.cs b/src/Core/Def/ContentType/IContentTypeRegistryService2.cs
new file mode 100644
index 0000000..adaf2b1
--- /dev/null
+++ b/src/Core/Def/ContentType/IContentTypeRegistryService2.cs
@@ -0,0 +1,24 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Utilities
+{
+ using System;
+ using Microsoft.VisualStudio.Utilities;
+
+ public interface IContentTypeRegistryService2 : IContentTypeRegistryService
+ {
+ /// <summary>
+ /// Get the mime type associated with a content type.
+ /// </summary>
+ /// <remarks>Use the <see cref="MimeTypeAttribute"/> attribute on a <see cref="ContentTypeDefinition"/> to associate a mime type with a content type.</remarks>
+ string GetMimeType(IContentType type);
+
+ /// <summary>
+ /// Get the content type associated with a mime type.
+ /// </summary>
+ /// <remarks>Use the <see cref="MimeTypeAttribute"/> attribute on a <see cref="ContentTypeDefinition"/> to associate a mime type with a content type.</remarks>
+ IContentType GetContentTypeForMimeType(string mimeType);
+ }
+}
diff --git a/src/Core/Def/ContentType/INameAndReplacesMetadata.cs b/src/Core/Def/ContentType/INameAndReplacesMetadata.cs
new file mode 100644
index 0000000..16a7fd0
--- /dev/null
+++ b/src/Core/Def/ContentType/INameAndReplacesMetadata.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Represents MEF metadata view corresponding to the <see cref="NameAttribute"/> and ReplacesAttributes.
+ /// </summary>
+ public interface INameAndReplacesMetadata
+ {
+ /// <summary>
+ /// Declared name value.
+ /// </summary>
+ [DefaultValue(null)]
+ string Name { get; }
+
+ /// <summary>
+ /// Declared Replaces values.
+ /// </summary>
+ [DefaultValue(null)]
+ IEnumerable<string> Replaces { get; }
+ }
+}
diff --git a/src/Core/Def/ContentType/INamedContentTypeMetadata.cs b/src/Core/Def/ContentType/INamedContentTypeMetadata.cs
new file mode 100644
index 0000000..4ae644b
--- /dev/null
+++ b/src/Core/Def/ContentType/INamedContentTypeMetadata.cs
@@ -0,0 +1,13 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Represents MEF metadata view combining <see cref="IContentTypeMetadata"/> and <see cref="INameAndReplacesMetadata"/> views.
+ /// </summary>
+ public interface INamedContentTypeMetadata : IContentTypeMetadata, INameAndReplacesMetadata
+ {
+ }
+}
diff --git a/src/Core/Def/ContentType/MimeTypeAttribute.cs b/src/Core/Def/ContentType/MimeTypeAttribute.cs
new file mode 100644
index 0000000..9935433
--- /dev/null
+++ b/src/Core/Def/ContentType/MimeTypeAttribute.cs
@@ -0,0 +1,41 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Utilities
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ /// <summary>
+ /// Declares an association between an extension part and a particular content type.
+ /// </summary>
+ [MetadataAttribute]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = false)]
+ public sealed class MimeTypeAttribute : SingletonBaseMetadataAttribute
+ {
+
+ /// <summary>
+ /// Initializes a new instance of <see cref="MimeTypeAttribute"/>.
+ /// </summary>
+ /// <param name="name">The Mime type to be associated with the content type.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/>is null or an empty string.</exception>
+ public MimeTypeAttribute(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ this.MimeType = name;
+ }
+
+ /// <summary>
+ /// The MimeType for the content type definition
+ /// </summary>
+ public string MimeType
+ {
+ get;
+ }
+ }
+}
diff --git a/src/Core/Def/CoreUtility.csproj b/src/Core/Def/CoreUtility.csproj
index f920b72..6426d95 100644
--- a/src/Core/Def/CoreUtility.csproj
+++ b/src/Core/Def/CoreUtility.csproj
@@ -1,75 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <PropertyGroup Label="BuildProps">
- <BuildPropsFile>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), Build.props))\Build.props</BuildPropsFile>
- </PropertyGroup>
- <Import Project="$(BuildPropsFile)" Condition="'$(BuildProps_Imported)'!='True' AND Exists('$(BuildPropsFile)') AND '$(VisualStudioDir)'==''" />
- <Import Project="..\Platform.Settings.targets" />
- <Import Project="$(PlatformPath)\Tools\Targets\Platform.Settings.Selector.targets" />
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<AssemblyName>Microsoft.VisualStudio.CoreUtility</AssemblyName>
- <OutputType>Library</OutputType>
- <OutputPath>$(BinariesDirectory)\bin\$(BuildArchitecture)</OutputPath>
- <SignAssemblyAttribute>true</SignAssemblyAttribute>
- <UseVsVersion>true</UseVsVersion>
- <AssemblyAttributeClsCompliant>true</AssemblyAttributeClsCompliant>
- <GenerateAssemblyRefs>true</GenerateAssemblyRefs>
- <NoWarn>649;436;$(NoWarn)</NoWarn>
- <GeneratedModuleId>Microsoft.VisualStudio.CoreUtility</GeneratedModuleId>
- <GeneratedModuleVersion>$(VsAssemblyVersion)</GeneratedModuleVersion>
- <BuildArchitecturesAllowed>$(BuildArchitecturesAllowed);amd64;arm</BuildArchitecturesAllowed>
- </PropertyGroup>
- <!-- IDE specific Information -->
- <PropertyGroup>
- <ProjectGuid>{BA3DD7EC-3F13-4400-A3A9-96AD425B3369}</ProjectGuid>
+ <TargetFramework>net46</TargetFramework>
</PropertyGroup>
+
<ItemGroup>
- <Compile Include="BaseUtility\ITelemetryIdProvider.cs" />
- <Compile Include="CoreUtilityAssemblyInfo.cs" />
- <Compile Include="BaseUtility\SingletonBaseMetadataAttribute.cs" />
- <Compile Include="BaseUtility\MultipleBaseMetadataAttribute.cs" />
- <Compile Include="BaseUtility\BaseDefinitionAttribute.cs" />
- <Compile Include="BaseUtility\DisplayNameAttribute.cs" />
- <Compile Include="BaseUtility\FxCopSuppressions.cs" />
- <Compile Include="BaseUtility\IOrderable.cs" />
- <Compile Include="BaseUtility\IPropertyOwner.cs" />
- <Compile Include="BaseUtility\NameAttribute.cs" />
- <Compile Include="BaseUtility\OrderAttribute.cs" />
- <Compile Include="BaseUtility\Orderer.cs" />
- <Compile Include="BaseUtility\PropertyCollection.cs" />
- <Compile Include="ContentType\FileExtensionToContentTypeDefinition.cs" />
- <Compile Include="ContentType\FileExtensionAttribute.cs" />
- <Compile Include="ContentType\FileNameAttribute.cs" />
- <Compile Include="ContentType\IContentTypeDefinition.cs" />
- <Compile Include="ContentType\IContentTypeDefinitionSource.cs" />
- <Compile Include="ContentType\IContentTypeRegistryService.cs" />
- <Compile Include="ContentType\IFileExtensionRegistryService.cs" />
- <Compile Include="ContentType\IFileExtensionRegistryService2.cs" />
- <Compile Include="ContentType\ContentTypeAttribute.cs" />
- <Compile Include="ContentType\ContentTypeDefinition.cs" />
- <Compile Include="ContentType\FxCopSuppressions.cs" />
- <Compile Include="ContentType\IContentType.cs" />
- <Reference Include="System" />
- <Reference Include="System.Core" />
- <Reference Include="System.ComponentModel.Composition" />
- <CopyFile Include="BaseUtility\Microsoft.VisualStudio.Utilities.Overview.mht">
- <DestFolder>$(SuiteBinPath)\PlatformOverviews</DestFolder>
- <Visible>false</Visible>
- </CopyFile>
- <CopyFile Include="BaseUtility\Microsoft.VisualStudio.Utilities.UsageGuide.mht">
- <DestFolder>$(SuiteBinPath)\PlatformOverviews</DestFolder>
- <Visible>false</Visible>
- </CopyFile>
+ <PackageReference Include="Microsoft.VisualStudio.Threading" Version="$(MicrosoftVisualStudioThreadingVersion)" />
+ <PackageReference Include="Microsoft.VisualStudio.Validation" Version="$(MicrosoftVisualStudioValidationVersion)" />
</ItemGroup>
+
<ItemGroup>
- <PublishPartCompiled Include="$(OutputPath)\$(AssemblyName).dll">
- <Visibility>Inter</Visibility>
- <FileType>Reference</FileType>
- </PublishPartCompiled>
+ <Reference Include="System.ComponentModel.Composition" />
</ItemGroup>
- <!--Import the targets-->
- <Import Project="$(PlatformPath)\Tools\Targets\Platform.Imports.targets" />
- <PropertyGroup>
- <CopyToSuiteBin>true</CopyToSuiteBin>
- </PropertyGroup>
+
</Project> \ No newline at end of file
diff --git a/src/Core/Impl/ContentType/ContentTypeImpl.cs b/src/Core/Impl/ContentType/ContentTypeImpl.cs
new file mode 100644
index 0000000..5a05136
--- /dev/null
+++ b/src/Core/Impl/ContentType/ContentTypeImpl.cs
@@ -0,0 +1,188 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Utilities.Implementation
+{
+ internal partial class ContentTypeImpl : IContentType
+ {
+ private readonly string name;
+ private readonly static IReadOnlyList<ContentTypeImpl> emptyBaseTypes = new ContentTypeImpl[0];
+ private IReadOnlyList<ContentTypeImpl> baseTypeList = emptyBaseTypes;
+
+ internal ContentTypeImpl(string name, string mimeType = null, IEnumerable<string> baseTypes = null)
+ {
+ this.name = name;
+ this.MimeType = mimeType;
+ this.UnprocessedBaseTypes = baseTypes;
+ }
+
+ public string TypeName
+ {
+ get { return this.name; }
+ }
+
+ public string DisplayName
+ {
+ get { return this.name; }
+ }
+
+ public string MimeType { get; }
+
+ public bool IsOfType(string type)
+ {
+ if (String.Compare(type, this.name, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return true;
+ }
+ else
+ {
+ foreach (IContentType baseType in this.baseTypeList)
+ {
+ if (baseType.IsOfType(type))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public IEnumerable<IContentType> BaseTypes
+ {
+ get { return this.baseTypeList; }
+ }
+
+ public override string ToString()
+ {
+ return this.name;
+ }
+
+ internal void ProcessBaseTypes(IDictionary<string, ContentTypeImpl> nameToContentTypeBuilder,
+ IDictionary<string, ContentTypeImpl> mimeTypeToContentTypeBuilder)
+ {
+ if (this.UnprocessedBaseTypes != null)
+ {
+ List<ContentTypeImpl> newBaseTypes = new List<ContentTypeImpl>();
+ foreach (var baseTypeName in this.UnprocessedBaseTypes)
+ {
+ // The expectation is that the base type will already exists but (if it doesn't) add a stub for it (& the ctor for a stub base type leaves it in a state
+ // where the basetypes/state are set appropriately).
+ var baseType = ContentTypeRegistryImpl.AddContentTypeFromMetadata(baseTypeName, /* mime type */null, /* base types */null, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder);
+ if (baseType == ContentTypeRegistryImpl.UnknownContentTypeImpl)
+ {
+ throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture,
+ Strings.ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown, this.TypeName));
+ }
+
+ if (!newBaseTypes.Contains(baseType))
+ newBaseTypes.Add(baseType);
+ }
+
+ if (newBaseTypes.Count > 0)
+ {
+ this.baseTypeList = newBaseTypes.ToArray();
+ this.state = VisitState.NotVisited;
+ }
+ else
+ {
+ Debug.Assert(object.ReferenceEquals(this.baseTypeList, emptyBaseTypes));
+ }
+
+ this.UnprocessedBaseTypes = null;
+ }
+ }
+
+ // used internally for cycle detection
+ internal enum VisitState
+ {
+ NotVisited = 0, // The node hasn't been visited yet
+ Visiting, // The node (or one of its children) is being visited
+ Visited // The node and its children have been visited before
+ }
+
+ private VisitState state = VisitState.Visited;
+
+ internal bool CheckForCycle(bool breakCycle)
+ {
+ try
+ {
+ if (this.baseTypeList.Count != 0)
+ {
+ this.state = VisitState.Visiting;
+ foreach (var baseType in this.baseTypeList)
+ {
+ if (baseType.state == VisitState.Visiting)
+ {
+ if (breakCycle)
+ {
+ // There is a cycle of this -> basetype -> ... -> this
+ // Don't try a surgical fix: simply break the cycle the easiest way possible
+ // since this is an error in the definitions that shouldn't happen.
+ // TODO: log the error.
+ this.baseTypeList = emptyBaseTypes;
+ }
+
+ return true;
+ }
+ else if ((baseType.state == VisitState.NotVisited) && baseType.CheckForCycle(breakCycle))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ finally
+ {
+ this.state = VisitState.Visited;
+ }
+
+ return false;
+ }
+
+ // used internally when building up content types
+ internal void AddUnprocessedBaseTypes(IEnumerable<string> newBaseTypes)
+ {
+ if (newBaseTypes != null)
+ {
+ if (object.ReferenceEquals(this.UnprocessedBaseTypes, emptyBaseTypes))
+ {
+ this.UnprocessedBaseTypes = newBaseTypes;
+ }
+ else
+ {
+ var allBaseTypes = new List<string>(this.UnprocessedBaseTypes);
+ allBaseTypes.AddRange(newBaseTypes);
+ this.UnprocessedBaseTypes = allBaseTypes;
+ }
+ }
+ }
+
+ internal IEnumerable<string> UnprocessedBaseTypes;
+
+#if DEBUG
+ internal bool IsProcessed
+ {
+ get
+ {
+ return (this.UnprocessedBaseTypes == null) && (this.baseTypeList != null) &&
+ ((this.baseTypeList.Count == 0)
+ ? (object.ReferenceEquals(this.baseTypeList, ContentTypeImpl.emptyBaseTypes) && (this.state == VisitState.Visited))
+ : (this.state == VisitState.NotVisited));
+ }
+ }
+
+ internal bool IsCheckedForCycles
+ {
+ get { return (this.state == VisitState.Visited) && (this.UnprocessedBaseTypes == null) && (object.ReferenceEquals(this.baseTypeList, ContentTypeImpl.emptyBaseTypes) || (this.baseTypeList.Count > 0)); }
+ }
+#endif
+ }
+}
diff --git a/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs
new file mode 100644
index 0000000..a5586a2
--- /dev/null
+++ b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs
@@ -0,0 +1,789 @@
+//
+// 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.
+//
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Utilities.Implementation
+{
+ public interface IContentTypeDefinitionMetadata
+ {
+ string Name { get; }
+
+ [System.ComponentModel.DefaultValue(null)]
+ IEnumerable<string> BaseDefinition { get; }
+
+ [System.ComponentModel.DefaultValue(null)]
+ string MimeType { get; }
+ }
+
+ [Export(typeof(IFileExtensionRegistryService))]
+ [Export(typeof(IFileExtensionRegistryService2))]
+ [Export(typeof(IContentTypeRegistryService))]
+ [Export(typeof(IContentTypeRegistryService2))]
+ internal sealed partial class ContentTypeRegistryImpl : IContentTypeRegistryService2, IFileExtensionRegistryService, IFileExtensionRegistryService2
+ {
+ [ImportMany]
+ internal List<Lazy<ContentTypeDefinition, IContentTypeDefinitionMetadata>> ContentTypeDefinitions { get; set; }
+
+ [ImportMany]
+ internal List<IContentTypeDefinitionSource> ExternalSources { get; set; }
+
+ [ImportMany]
+ internal List<Lazy<FileExtensionToContentTypeDefinition, IFileToContentTypeMetadata>> FileToContentTypeProductions { get; set; }
+
+ private MapCollection maps;
+
+ /// <summary>
+ /// The name of the unknown content type, guaranteed to exists no matter what other content types are produced
+ /// </summary>
+ private const string UnknownContentTypeName = "UNKNOWN";
+ internal readonly static ContentTypeImpl UnknownContentTypeImpl = new ContentTypeImpl(ContentTypeRegistryImpl.UnknownContentTypeName, null, null);
+
+ /// <summary>
+ /// Builds the list of available content types
+ /// Note: This function must be called after acquiring a lock on syncLock
+ /// </summary>
+ /// <remarks>
+ /// Building the content type mappings should not throw exceptions, but should rather be logging issues
+ /// with some kind of common error reporting service and try to recover by ignoring the asset productions
+ /// that are deemed to cause the problem.
+ /// </remarks>
+ private void BuildContentTypes()
+ {
+ var oldMaps = Volatile.Read(ref this.maps);
+ if (oldMaps == null)
+ {
+ var nameToContentTypeBuilder = MapCollection.Empty.NameToContentTypeMap.ToBuilder();
+ var mimeTypeToContentTypeBuilder = MapCollection.Empty.MimeTypeToContentTypeMap.ToBuilder();
+
+ // Add the singleton Unknown content type to the dictionary
+ nameToContentTypeBuilder.Add(ContentTypeRegistryImpl.UnknownContentTypeName, ContentTypeRegistryImpl.UnknownContentTypeImpl);
+
+
+ // For each content type provision, create an IContentType.
+ foreach (Lazy<ContentTypeDefinition, IContentTypeDefinitionMetadata> contentTypeDefinition in ContentTypeDefinitions)
+ {
+ AddContentTypeFromMetadata(contentTypeDefinition.Metadata.Name,
+ contentTypeDefinition.Metadata.MimeType,
+ contentTypeDefinition.Metadata.BaseDefinition, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder);
+ }
+
+ // Now consider the external sources. This allows us to consider legacy content types together with MEF-defined
+ // content types.
+ foreach (IContentTypeDefinitionSource source in this.ExternalSources)
+ {
+ if (source.Definitions != null)
+ {
+ foreach (IContentTypeDefinition metadata in source.Definitions)
+ {
+ AddContentTypeFromMetadata(metadata.Name,
+ /* mimeType*/ null,
+ metadata.BaseDefinitions, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder);
+ }
+ }
+ }
+
+ List<ContentTypeImpl> allTypes = new List<ContentTypeImpl>(nameToContentTypeBuilder.Count);
+ allTypes.AddRange(nameToContentTypeBuilder.Values);
+ foreach (var type in allTypes)
+ {
+ type.ProcessBaseTypes(nameToContentTypeBuilder, mimeTypeToContentTypeBuilder);
+ }
+
+#if DEBUG
+ foreach (var type in nameToContentTypeBuilder.Values)
+ {
+ Debug.Assert(type.IsProcessed);
+ }
+#endif
+
+ foreach (var type in nameToContentTypeBuilder.Values)
+ {
+ type.CheckForCycle(breakCycle: true);
+ }
+
+ var fileExtensionToContentTypeMapBuilder = MapCollection.Empty.FileExtensionToContentTypeMap.ToBuilder();
+ var fileNameToContentTypeMapBuilder = MapCollection.Empty.FileNameToContentTypeMap.ToBuilder();
+ foreach (var fileExtensionDefinition in this.FileToContentTypeProductions)
+ {
+ // MEF ensures that there will be at least one content type in the metadata. We take the first one.
+ // We prefer this over defining a different attribute from ContentType[] for this purpose.
+ var contentTypeName = fileExtensionDefinition.Metadata.ContentTypes.FirstOrDefault();
+ ContentTypeImpl contentType;
+ if ((contentTypeName != null) && nameToContentTypeBuilder.TryGetValue(contentTypeName, out contentType))
+ {
+ if (!string.IsNullOrEmpty(fileExtensionDefinition.Metadata.FileExtension))
+ {
+ foreach (var ext in fileExtensionDefinition.Metadata.FileExtension.Split(';'))
+ {
+ if (ext != null)
+ {
+ var extension = RemoveExtensionDot(ext);
+ if (!(string.IsNullOrWhiteSpace(extension) || fileExtensionToContentTypeMapBuilder.ContainsKey(extension)))
+ fileExtensionToContentTypeMapBuilder.Add(extension, contentType);
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(fileExtensionDefinition.Metadata.FileName))
+ {
+ foreach (var name in fileExtensionDefinition.Metadata.FileName.Split(';'))
+ {
+ if (!(string.IsNullOrWhiteSpace(name) || fileNameToContentTypeMapBuilder.ContainsKey(name)))
+ fileNameToContentTypeMapBuilder.Add(name, contentType);
+ }
+ }
+ }
+ }
+
+ var newMaps = new MapCollection(nameToContentTypeBuilder.ToImmutable(), mimeTypeToContentTypeBuilder.ToImmutable(), fileExtensionToContentTypeMapBuilder.ToImmutable(), fileNameToContentTypeMapBuilder.ToImmutable());
+ Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+
+ // We actually don't care whether or not the CompareExchange succeeded.
+ // Eitehr it succeeded (normally the case) or someone else successfully completed BuildContentTypes on another thread and we shouldn't do anything.
+ }
+ }
+
+ private const string BaseMimePrefix = @"text/";
+ private const string MimePrefix = BaseMimePrefix + @"x-";
+
+ internal static ContentTypeImpl AddContentTypeFromMetadata(string contentTypeName, string mimeType, IEnumerable<string> baseTypes,
+ IDictionary<string, ContentTypeImpl> nameToContentTypeBuilder,
+ IDictionary<string, ContentTypeImpl> mimeTypeToContentTypeBuilder)
+ {
+ if (!string.IsNullOrEmpty(contentTypeName))
+ {
+ ContentTypeImpl type;
+ if (!nameToContentTypeBuilder.TryGetValue(contentTypeName, out type))
+ {
+ bool addToMimeTypeMap = false;
+ if (string.IsNullOrWhiteSpace(mimeType))
+ {
+ mimeType = MimePrefix + contentTypeName.ToLowerInvariant();
+ }
+ else if (mimeTypeToContentTypeBuilder.ContainsKey(mimeType))
+ {
+ mimeType = null;
+ }
+ else
+ {
+ addToMimeTypeMap = true;
+ }
+
+ type = new ContentTypeImpl(contentTypeName, mimeType, baseTypes);
+
+ nameToContentTypeBuilder.Add(contentTypeName, type);
+ if (addToMimeTypeMap)
+ {
+ mimeTypeToContentTypeBuilder.Add(mimeType, type);
+ }
+ }
+ else
+ {
+ type.AddUnprocessedBaseTypes(baseTypes);
+ }
+
+ return type;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Checks whether the specified type is base type for another content type
+ /// </summary>
+ /// <param name="typeToCheck">The type to check for being a base type</param>
+ /// <param name="derivedType">An out parameter to receive the first discovered derived type</param>
+ /// <returns><c>True</c> if the given <paramref name="typeToCheck"/> content type is a base type</returns>
+ private bool IsBaseType(ContentTypeImpl typeToCheck, out ContentTypeImpl derivedType)
+ {
+ derivedType = null;
+
+ foreach (ContentTypeImpl type in this.maps.NameToContentTypeMap.Values)
+ {
+ if (type != typeToCheck)
+ {
+ foreach (IContentType baseType in type.BaseTypes)
+ {
+ if (baseType == typeToCheck)
+ {
+ derivedType = type;
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ #region IContentTypeRegistryService Members
+ public IContentType GetContentType(string typeName)
+ {
+ if (string.IsNullOrWhiteSpace(typeName))
+ {
+ throw new ArgumentException(nameof(typeName));
+ }
+
+ this.BuildContentTypes();
+
+ ContentTypeImpl contentType = null;
+ this.maps.NameToContentTypeMap.TryGetValue(typeName, out contentType);
+
+ return contentType;
+ }
+
+ public IContentType UnknownContentType
+ {
+ get { return ContentTypeRegistryImpl.UnknownContentTypeImpl; }
+ }
+
+ public IEnumerable<IContentType> ContentTypes
+ {
+ get
+ {
+ this.BuildContentTypes();
+ var map = this.maps.NameToContentTypeMap;
+
+ return map.Values;
+ }
+ }
+
+ public IContentType AddContentType(string typeName, IEnumerable<string> baseTypeNames)
+ {
+ if (string.IsNullOrWhiteSpace(typeName))
+ {
+ throw new ArgumentException(nameof(typeName));
+ }
+
+ // This has the side effect of building the content types.
+ if (this.GetContentType(typeName) != null)
+ {
+ // Cannot dynamically add a new content type if a content type with the same name already exists
+ throw new ArgumentException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotAddExistentType, typeName));
+ }
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ var nameToContentTypeMap = new PseudoBuilder<string, ContentTypeImpl>(oldMaps.NameToContentTypeMap);
+ var mimeTypeToContentTypeMap = new PseudoBuilder<string, ContentTypeImpl>(oldMaps.MimeTypeToContentTypeMap);
+
+ var type = AddContentTypeFromMetadata(typeName, null, baseTypeNames,
+ nameToContentTypeMap, mimeTypeToContentTypeMap);
+
+ type.ProcessBaseTypes(nameToContentTypeMap, mimeTypeToContentTypeMap);
+
+ if (type.CheckForCycle(breakCycle: false))
+ {
+ throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CausesCycles, type.TypeName));
+ }
+
+ var newMaps = new MapCollection(nameToContentTypeMap.Source, mimeTypeToContentTypeMap.Source, oldMaps.FileExtensionToContentTypeMap, oldMaps.FileNameToContentTypeMap);
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return type;
+ }
+
+ // Two people tried to add content types simultaneously.
+ oldMaps = results;
+ }
+ }
+
+ public void RemoveContentType(string typeName)
+ {
+ if (string.IsNullOrWhiteSpace(typeName))
+ {
+ throw new ArgumentException(nameof(typeName));
+ }
+
+ this.BuildContentTypes();
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ ContentTypeImpl type;
+ if (!oldMaps.NameToContentTypeMap.TryGetValue(typeName, out type))
+ {
+ // No type == no type to remove;
+ return;
+ }
+
+ if (type == ContentTypeRegistryImpl.UnknownContentTypeImpl)
+ {
+ // Check if the type to be removed is not the Unknown content type
+ throw new InvalidOperationException(Strings.ContentTypeRegistry_CannotRemoveTheUnknownType);
+ }
+
+ ContentTypeImpl derivedType;
+ if (IsBaseType(type, out derivedType))
+ {
+ // Check if the type is base type for another registered type
+ throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveBaseType, type.TypeName, derivedType.TypeName));
+ }
+
+ // If there are file extensions using this content type we won't allow removing it
+ if (this.maps.FileExtensionToContentTypeMap.Values.Any(c => c == type))
+ {
+ // If there are file extensions using this content type we won't allow removing it
+ throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName));
+ }
+
+ // If there are file extensions using this content type we won't allow removing it
+ if (this.maps.FileNameToContentTypeMap.Values.Any(c => c == type))
+ {
+ // If there are file extensions using this content type we won't allow removing it
+ throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName));
+ }
+
+ var newMaps = new MapCollection(oldMaps.NameToContentTypeMap.Remove(typeName),
+ (type.MimeType != null) ? oldMaps.MimeTypeToContentTypeMap.Remove(type.MimeType) : oldMaps.MimeTypeToContentTypeMap,
+ oldMaps.FileExtensionToContentTypeMap, oldMaps.FileNameToContentTypeMap);
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return;
+ }
+
+ // Two people tried to remove content types simultaneously.
+ oldMaps = results;
+ }
+ }
+ #endregion
+
+ #region IContentTypeRegistryService2 Members
+ public string GetMimeType(IContentType type)
+ {
+ var typeImpl = type as ContentTypeImpl;
+ if (typeImpl == null)
+ {
+ throw new ArgumentException(nameof(type));
+ }
+ else if (typeImpl == UnknownContentTypeImpl)
+ {
+ return null;
+ }
+
+ return typeImpl.MimeType;
+ }
+
+ public IContentType GetContentTypeForMimeType(string mimeType)
+ {
+ if (string.IsNullOrWhiteSpace(mimeType))
+ {
+ throw new ArgumentException(nameof(mimeType));
+ }
+
+ this.BuildContentTypes();
+
+ ContentTypeImpl contentType = null;
+ if (!this.maps.MimeTypeToContentTypeMap.TryGetValue(mimeType, out contentType))
+ {
+ if (mimeType.StartsWith(BaseMimePrefix))
+ {
+ if (!(mimeType.StartsWith(MimePrefix) && this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(MimePrefix.Length), out contentType)))
+ {
+ this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(BaseMimePrefix.Length), out contentType);
+ }
+ }
+ }
+
+ return contentType;
+ }
+ #endregion
+
+ #region IFileExtensionRegistryService Members
+ public IContentType GetContentTypeForExtension(string extension)
+ {
+ if (extension == null)
+ {
+ throw new ArgumentNullException(nameof(extension));
+ }
+
+ this.BuildContentTypes();
+
+ ContentTypeImpl contentType = null;
+ this.maps.FileExtensionToContentTypeMap.TryGetValue(RemoveExtensionDot(extension), out contentType);
+
+ // TODO: should we return null if contentType is null?
+ return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl;
+ }
+
+ public IEnumerable<string> GetExtensionsForContentType(IContentType contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ this.BuildContentTypes();
+
+ // We don't expect this to be called on a perf critical thread so we can use the dictionary.
+ foreach (var kvp in this.maps.FileExtensionToContentTypeMap)
+ {
+ if (contentType == kvp.Value)
+ {
+ yield return kvp.Key;
+ }
+ }
+ }
+
+ public void AddFileExtension(string extension, IContentType contentType)
+ {
+ if (string.IsNullOrWhiteSpace(extension))
+ {
+ throw new ArgumentException(nameof(extension));
+ }
+
+ var contentTypeImpl = contentType as ContentTypeImpl;
+ if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl))
+ {
+ throw new ArgumentException(nameof(contentType));
+ }
+
+ this.BuildContentTypes();
+ extension = RemoveExtensionDot(extension);
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ ContentTypeImpl type;
+ if (oldMaps.FileExtensionToContentTypeMap.TryGetValue(extension, out type))
+ {
+ if (type != contentTypeImpl)
+ {
+ throw new InvalidOperationException
+ (String.Format(System.Globalization.CultureInfo.CurrentUICulture,
+ Strings.FileExtensionRegistry_NoMultipleContentTypes, extension));
+ }
+
+ return;
+ }
+
+ var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap,
+ oldMaps.FileExtensionToContentTypeMap.Add(extension, contentTypeImpl),
+ oldMaps.FileNameToContentTypeMap);
+
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return;
+ }
+
+ // Two people tried to remove content types simultaneously.
+ oldMaps = results;
+ }
+ }
+
+ public void RemoveFileExtension(string extension)
+ {
+ if (extension == null)
+ {
+ throw new ArgumentNullException(nameof(extension));
+ }
+
+ this.BuildContentTypes();
+
+ extension = RemoveExtensionDot(extension);
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ if (!oldMaps.FileExtensionToContentTypeMap.ContainsKey(extension))
+ {
+ return;
+ }
+
+ var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap,
+ oldMaps.FileExtensionToContentTypeMap.Remove(extension),
+ oldMaps.FileNameToContentTypeMap);
+
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return;
+ }
+
+ // Two people tried to remove content types simultaneously.
+ oldMaps = results;
+ }
+ }
+ #endregion
+
+ #region IFileExtensionRegistryService2 Members
+ public IContentType GetContentTypeForFileName(string fileName)
+ {
+ if (fileName == null)
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ this.BuildContentTypes();
+
+ ContentTypeImpl contentType = null;
+ this.maps.FileNameToContentTypeMap.TryGetValue(fileName, out contentType);
+
+ // TODO: should we return null if contentType is null?
+ return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl;
+ }
+
+ public IContentType GetContentTypeForFileNameOrExtension(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ // No need to lock, we are calling locking public method.
+ var fileNameContentType = this.GetContentTypeForFileName(name);
+
+ // Attempt to use extension as fallback ContentType if file name isn't recognized.
+ if (fileNameContentType == ContentTypeRegistryImpl.UnknownContentTypeImpl)
+ {
+ var extension = Path.GetExtension(name);
+
+ if (!string.IsNullOrEmpty(extension))
+ {
+ // No need to lock, we are calling locking public method.
+ return this.GetContentTypeForExtension(extension);
+ }
+ }
+
+ return fileNameContentType;
+ }
+
+ public IEnumerable<string> GetFileNamesForContentType(IContentType contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ this.BuildContentTypes();
+
+ // We don't expect this to be called on a perf critical thread so we can use the dictionary.
+ foreach (var kvp in this.maps.FileNameToContentTypeMap)
+ {
+ if (contentType == kvp.Value)
+ {
+ yield return kvp.Key;
+ }
+ }
+ }
+
+ public void AddFileName(string fileName, IContentType contentType)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentException(nameof(fileName));
+ }
+
+ var contentTypeImpl = contentType as ContentTypeImpl;
+ if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl))
+ {
+ throw new ArgumentException(nameof(contentType));
+ }
+
+ this.BuildContentTypes();
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ ContentTypeImpl type;
+ if (oldMaps.FileNameToContentTypeMap.TryGetValue(fileName, out type))
+ {
+ if (type != contentTypeImpl)
+ {
+ throw new InvalidOperationException
+ (String.Format(System.Globalization.CultureInfo.CurrentUICulture,
+ Strings.FileExtensionRegistry_NoMultipleContentTypes, fileName));
+ }
+
+ return;
+ }
+
+ var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap,
+ oldMaps.FileExtensionToContentTypeMap,
+ oldMaps.FileNameToContentTypeMap.Add(fileName, contentTypeImpl));
+
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return;
+ }
+
+ // Two people tried to remove content types simultaneously.
+ oldMaps = results;
+ }
+ }
+
+ public void RemoveFileName(string fileName)
+ {
+ if (fileName == null)
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ this.BuildContentTypes();
+
+ var oldMaps = Volatile.Read(ref this.maps);
+ while (true)
+ {
+ if (!oldMaps.FileNameToContentTypeMap.ContainsKey(fileName))
+ {
+ return;
+ }
+
+ var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap,
+ oldMaps.FileExtensionToContentTypeMap,
+ oldMaps.FileNameToContentTypeMap.Remove(fileName));
+
+ var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps);
+ if (results == oldMaps)
+ {
+ return;
+ }
+
+ // Two people tried to remove content types simultaneously.
+ oldMaps = results;
+ }
+ }
+ #endregion
+
+ private static string RemoveExtensionDot(string extension)
+ {
+ if (extension.StartsWith("."))
+ {
+ return extension.TrimStart('.');
+ }
+ else
+ {
+ return extension;
+ }
+ }
+
+ class MapCollection
+ {
+ public readonly static MapCollection Empty = new MapCollection();
+
+ public readonly ImmutableDictionary<string, ContentTypeImpl> NameToContentTypeMap;
+ public readonly ImmutableDictionary<string, ContentTypeImpl> MimeTypeToContentTypeMap;
+ public readonly ImmutableDictionary<string, ContentTypeImpl> FileExtensionToContentTypeMap;
+ public readonly ImmutableDictionary<string, ContentTypeImpl> FileNameToContentTypeMap;
+
+ private MapCollection()
+ {
+ this.NameToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
+ this.MimeTypeToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.Ordinal);
+ this.FileExtensionToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
+ this.FileNameToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public MapCollection(ImmutableDictionary<string, ContentTypeImpl> nameToContentType, ImmutableDictionary<string, ContentTypeImpl> mimeTypeToContentTypeMap,
+ ImmutableDictionary<string, ContentTypeImpl> fileExtensionToContentTypeMap, ImmutableDictionary<string, ContentTypeImpl> fileNameToContentTypeMap)
+ {
+ this.NameToContentTypeMap = nameToContentType;
+ this.MimeTypeToContentTypeMap = mimeTypeToContentTypeMap;
+ this.FileExtensionToContentTypeMap = fileExtensionToContentTypeMap;
+ this.FileNameToContentTypeMap = fileNameToContentTypeMap;
+
+#if DEBUG
+ foreach (var c in nameToContentType.Values)
+ {
+ Debug.Assert(c.IsCheckedForCycles);
+ }
+#endif
+ }
+ }
+
+ class PseudoBuilder<K, V> : IDictionary<K, V>
+ {
+ public ImmutableDictionary<K, V> Source { get; private set; }
+
+ public PseudoBuilder(ImmutableDictionary<K, V> source)
+ {
+ this.Source = source;
+ }
+
+ public void Add(K key, V value)
+ {
+ this.Source = this.Source.Add(key, value);
+ }
+
+ public bool ContainsKey(K key)
+ {
+ return this.Source.ContainsKey(key);
+ }
+
+ public bool TryGetValue(K key, out V value)
+ {
+ return this.Source.TryGetValue(key, out value);
+ }
+
+ #region NotImplemented
+ public V this[K key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public ICollection<K> Keys => throw new NotImplementedException();
+
+ public ICollection<V> Values => throw new NotImplementedException();
+
+ public int Count => throw new NotImplementedException();
+
+ public bool IsReadOnly => throw new NotImplementedException();
+
+ public void Add(KeyValuePair<K, V> item)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Clear()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Contains(KeyValuePair<K, V> item)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(K key)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool Remove(KeyValuePair<K, V> item)
+ {
+ throw new NotImplementedException();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ #endregion
+ }
+ }
+}
diff --git a/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs
new file mode 100644
index 0000000..4e3bbd9
--- /dev/null
+++ b/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs
@@ -0,0 +1,19 @@
+//
+// 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.Utilities.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+
+ public interface IFileExtensionToContentTypeMetadata
+ {
+ string FileExtension { get; }
+ IEnumerable<string> ContentTypes { get; }
+ }
+}
+
diff --git a/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs
new file mode 100644
index 0000000..48ec87a
--- /dev/null
+++ b/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs
@@ -0,0 +1,19 @@
+//
+// 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.Utilities.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+
+ public interface IFileNameToContentTypeMetadata
+ {
+ string FileName { get; }
+ IEnumerable<string> ContentTypes { get; }
+ }
+}
+
diff --git a/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs
new file mode 100644
index 0000000..78dc2a6
--- /dev/null
+++ b/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs
@@ -0,0 +1,24 @@
+//
+// 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.Utilities.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+
+ public interface IFileToContentTypeMetadata
+ {
+ [System.ComponentModel.DefaultValue(null)]
+ string FileExtension { get; }
+
+ [System.ComponentModel.DefaultValue(null)]
+ string FileName { get; }
+
+ IEnumerable<string> ContentTypes { get; }
+ }
+}
+
diff --git a/src/Core/Impl/ContentType/StringToContentTypesMap.cs b/src/Core/Impl/ContentType/StringToContentTypesMap.cs
new file mode 100644
index 0000000..52fae34
--- /dev/null
+++ b/src/Core/Impl/ContentType/StringToContentTypesMap.cs
@@ -0,0 +1,129 @@
+//
+// 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.Utilities.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Linq;
+
+ internal sealed class StringToContentTypesMap
+ {
+ // Map of extensions to their corresponding content types
+ private Dictionary<string, IContentType> stringMap;
+
+ public StringToContentTypesMap(IEnumerable<Tuple<string, IContentType>> mappings)
+ {
+
+ if (mappings == null)
+ {
+ throw new ArgumentNullException(nameof(mappings));
+ }
+
+ this.stringMap = new Dictionary<string, IContentType>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var mapping in mappings)
+ {
+ // Any failures should ideally be logged somehow.
+ TryAddString(mapping.Item1, mapping.Item2);
+ }
+ }
+
+ public IContentType GetContentTypeForString(string str)
+ {
+ if (str == null)
+ {
+ throw new ArgumentNullException(nameof(str));
+ }
+
+ IContentType contentType;
+
+ if (!this.stringMap.TryGetValue(str, out contentType))
+ {
+ return null;
+ }
+
+ return contentType;
+ }
+
+ public IEnumerable<string> GetStringsForContentType(IContentType contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ // Asymptotically slow, however, after searching the VS code base, we found that
+ // looking up extensions for ContentType is only used by tests, and is probably
+ // rarely used. This method used be backed by a second map for quick lookup,
+ // but for simplicity, we're going to move to a single map, barring any regressions.
+ return this.stringMap
+ .Where(pair => pair.Value == contentType)
+ .Select(pair => pair.Key);
+ }
+
+ public void AddMapping(string str, IContentType contentType)
+ {
+ if (str == null)
+ {
+ throw new ArgumentNullException(nameof(str));
+ }
+
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ if (!TryAddString(str, contentType))
+ {
+ throw new InvalidOperationException
+ (String.Format(System.Globalization.CultureInfo.CurrentUICulture,
+ Strings.FileExtensionRegistry_NoMultipleContentTypes, str));
+ }
+ }
+
+ public void RemoveMapping(string str)
+ {
+ if (str == null)
+ {
+ throw new ArgumentNullException(nameof(str));
+ }
+
+ this.stringMap.Remove(str);
+ }
+
+ private bool TryAddString(string str, IContentType contentType)
+ {
+ if (str == null)
+ {
+ throw new ArgumentNullException(nameof(str));
+ }
+
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ // Check if the string is already registered
+ IContentType existingContentType;
+ if (this.stringMap.TryGetValue(str, out existingContentType))
+ {
+ // Return false if there is a conflict.
+ return contentType == existingContentType;
+ }
+ else
+ {
+ // A new string - lets add it to the map
+ this.stringMap.Add(str, contentType);
+ }
+
+ return true;
+ }
+ }
+}
+
diff --git a/src/Core/Impl/ContentType/Strings.Designer.cs b/src/Core/Impl/ContentType/Strings.Designer.cs
new file mode 100644
index 0000000..736d04f
--- /dev/null
+++ b/src/Core/Impl/ContentType/Strings.Designer.cs
@@ -0,0 +1,153 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Utilities.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.CoreUtilityImplementation.ContentType.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The content type {0} has already been defined..
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotAddExistentType {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotAddExistentType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot add content type with empty or undefined type name..
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotAddTypeWithEmptyTypeName {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotAddTypeWithEmptyTypeName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There can only be one &quot;Unknown&quot; content type defined. .
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotProduceAnotherUnknownType {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotProduceAnotherUnknownType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} cannot be removed because it&apos;s used as base type for the content type {1}..
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotRemoveBaseType {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveBaseType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;Unknown&quot; content type cannot be removed..
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotRemoveTheUnknownType {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveTheUnknownType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} cannot be removed because it&apos;s associated with one or more file extensions..
+ /// </summary>
+ internal static string ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The content type {0} cannot be used as base for content type {1} because it would create a derivation cycle..
+ /// </summary>
+ internal static string ContentTypeRegistry_CausesCycles {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_CausesCycles", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Content type {0} cannot be added. No content type can be derive from the &quot;Unknown&quot; content type. .
+ /// </summary>
+ internal static string ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown.
+ /// </summary>
+ internal static string ContentTypeRegistry_UnknownTypeDisplayName {
+ get {
+ return ResourceManager.GetString("ContentTypeRegistry_UnknownTypeDisplayName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} file name or extension cannot be associated with multiple content types..
+ /// </summary>
+ internal static string FileExtensionRegistry_NoMultipleContentTypes {
+ get {
+ return ResourceManager.GetString("FileExtensionRegistry_NoMultipleContentTypes", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Core/Impl/ContentType/Strings.resx b/src/Core/Impl/ContentType/Strings.resx
new file mode 100644
index 0000000..2591a20
--- /dev/null
+++ b/src/Core/Impl/ContentType/Strings.resx
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="FileExtensionRegistry_NoMultipleContentTypes" xml:space="preserve">
+ <value>{0} file name or extension cannot be associated with multiple content types.</value>
+ </data>
+ <data name="ContentTypeRegistry_UnknownTypeDisplayName" xml:space="preserve">
+ <value>Unknown</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotAddTypeWithEmptyTypeName" xml:space="preserve">
+ <value>Cannot add content type with empty or undefined type name.</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotProduceAnotherUnknownType" xml:space="preserve">
+ <value>There can only be one "Unknown" content type defined. </value>
+ </data>
+ <data name="ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown" xml:space="preserve">
+ <value>Content type {0} cannot be added. No content type can be derive from the "Unknown" content type. </value>
+ </data>
+ <data name="ContentTypeRegistry_CausesCycles" xml:space="preserve">
+ <value>The content type {0} leads to a cycle in its base types.</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotAddExistentType" xml:space="preserve">
+ <value>The content type {0} has already been defined.</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotRemoveTheUnknownType" xml:space="preserve">
+ <value>The "Unknown" content type cannot be removed.</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotRemoveBaseType" xml:space="preserve">
+ <value>The type {0} cannot be removed because it's used as base type for the content type {1}.</value>
+ </data>
+ <data name="ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions" xml:space="preserve">
+ <value>The type {0} cannot be removed because it's associated with one or more file extensions.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs b/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs
new file mode 100644
index 0000000..070dc76
--- /dev/null
+++ b/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs
@@ -0,0 +1,364 @@
+//
+// 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.Language.StandardClassification.Implementation
+{
+ using System.ComponentModel.Composition;
+ using System.Windows.Media;
+
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Classification;
+
+ // =========== Classification formats ================
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekBackground)]
+ [Name("Peek Background")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekBackgroundClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekBackgroundClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekBackground;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0xF2, 0xF8, 0xFC);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekBackgroundUnfocused)]
+ [Name("Peek Background Unfocused")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekBackgroundUnfocusedClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekBackgroundUnfocusedClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekBackgroundUnfocused;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0xEB, 0xEB, 0xEB);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHistorySelected)]
+ [Name("Peek History Selected")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekHistorySelectedClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekHistorySelectedClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekBreadcrumbSelected;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0, 0x7A, 0xCC);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHistoryHovered)]
+ [Name("Peek History Hovered")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekHistoryHoveredClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekHistoryHoveredClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekBreadcrumbHovered;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0x1C, 0x97, 0xEA);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekFocusedBorder)]
+ [Name("Peek Focused Border")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekFocusedBorderClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekFocusedBorderClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekFocusedBorder;
+ BackgroundCustomizable = false;
+ ForegroundColor = Color.FromRgb(0, 0x7A, 0xCC);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekLabelText)]
+ [Name("Peek Label Text")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekLabelTextClassificationFormatDefinition: ClassificationFormatDefinition
+ {
+ public PeekLabelTextClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekLabelText;
+ BackgroundCustomizable = false;
+ ForegroundColor = Color.FromRgb(0, 0, 0);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHighlightedText)]
+ [Name("Peek Highlighted Text")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekHighlightedTextClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekHighlightedTextClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekHighlightedText;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0xC1, 0xDE, 0xF1);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHighlightedTextUnfocused)]
+ [Name("Peek Highlighted Text Unfocused")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class PeekHighlightedTextUnfocusedClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PeekHighlightedTextUnfocusedClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PeekHighlightedTextUnfocused;
+ ForegroundCustomizable = false;
+ BackgroundColor = Color.FromRgb(0xD2, 0xDA, 0xE0);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Comment)]
+ [Name("Comment")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Excluded Code")]
+ internal class CommentClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public CommentClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Comment;
+ ForegroundColor = Color.FromRgb(0x00, 0x80, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.ExcludedCode)]
+ [Name("Excluded Code")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Identifier")]
+ internal class PreprocessorExcludeClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PreprocessorExcludeClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_ExcludedCode;
+ ForegroundColor = Colors.Gray;
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Identifier)]
+ [Name("Identifier")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Keyword")]
+ internal class IdentifierClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public IdentifierClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Identifier;
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Keyword)]
+ [Name("Keyword")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Literal")]
+ internal class KeywordClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public KeywordClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Keyword;
+ ForegroundColor = Color.FromRgb(0x00, 0x00, 0xFF);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PreprocessorKeyword)]
+ [Name("Preprocessor Keyword")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "String")]
+ internal class PreprocessorKeywordClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public PreprocessorKeywordClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_PreprocessorKeyword;
+ ForegroundColor = Color.FromRgb(0x80, 0x80, 0x80);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Operator)]
+ [Name("Operator")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Preprocessor Keyword")]
+ internal class OperatorClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public OperatorClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Operator;
+ ForegroundColor = Color.FromRgb(0x00, 0x00, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Literal)]
+ [Name("Literal")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Number")]
+ internal class LiteralClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public LiteralClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Literal;
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupAttribute)]
+ [Name("Markup Attribute")]
+ [UserVisible(true)]
+ [Order(After = "Literal", Before = "Number")]
+ internal class MarkupAttributeFormatDefinition : ClassificationFormatDefinition
+ {
+ public MarkupAttributeFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_MarkupAttribute;
+ ForegroundColor = Color.FromRgb(0xFF, 0x00, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupAttributeValue)]
+ [Name("Markup Attribute Value")]
+ [UserVisible(true)]
+ [Order(After = "Literal", Before = "Number")]
+ internal class MarkupAttributeValueFormatDefinition : ClassificationFormatDefinition
+ {
+ public MarkupAttributeValueFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_MarkupAttributeValue;
+ ForegroundColor = Color.FromRgb(0x00, 0x00, 0xFF);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupNode)]
+ [Name("Markup Node")]
+ [UserVisible(true)]
+ [Order(After = "Literal", Before = "Number")]
+ internal class MarkupNodeFormatDefinition : ClassificationFormatDefinition
+ {
+ public MarkupNodeFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_MarkupNode;
+ ForegroundColor = Color.FromRgb(0x80, 0x00, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.String)]
+ [Name("String")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "SymbolDefinitionClassificationFormat")]
+ internal class StringClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public StringClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_String;
+ ForegroundColor = Color.FromRgb(0xA3, 0x15, 0x15);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Type)]
+ [Name("Type")]
+ [UserVisible(true)]
+ [Order(After = "String", Before = "Number")]
+ internal class TypeFormatDefinition : ClassificationFormatDefinition
+ {
+ public TypeFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Type;
+ ForegroundColor = Color.FromRgb(0x2B, 0x91, 0xAF);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Number)]
+ [Name("Number")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "Operator")]
+ internal class StringCharacterNumericalClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public StringCharacterNumericalClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_Number;
+ ForegroundColor = Color.FromRgb(0x00, 0x00, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.SymbolDefinition)]
+ [Name("SymbolDefinitionClassificationFormat")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = "SymbolReferenceClassificationFormat")]
+ internal class SymbolDefinitionClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public SymbolDefinitionClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_SymbolDefinition;
+ ForegroundColor = Color.FromRgb(0x2B, 0x91, 0xAF);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.SymbolReference)]
+ [Name("SymbolReferenceClassificationFormat")]
+ [UserVisible(true)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
+ internal class SymbolReferenceClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ public SymbolReferenceClassificationFormatDefinition()
+ {
+ DisplayName = Strings.EditorFormat_SymbolReference;
+ IsBold = true;
+ ForegroundColor = Color.FromRgb(0x00, 0x88, 0x00);
+ }
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [Name(LanguagePriority.NaturalLanguage)]
+ [UserVisible(false)]
+ [Order(After = Priority.Default, Before = LanguagePriority.FormalLanguage)]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.NaturalLanguage)]
+ internal class NaturalLanguagePriorityClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ }
+
+ [Export(typeof(EditorFormatDefinition))]
+ [Name(LanguagePriority.FormalLanguage)]
+ [UserVisible(false)]
+ [Order(After = LanguagePriority.NaturalLanguage, Before = Priority.High)]
+ [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.FormalLanguage)]
+ internal class FormalLanguagePriorityClassificationFormatDefinition : ClassificationFormatDefinition
+ {
+ }
+}
diff --git a/src/Language/Impl/StandardClassification/StandardClassificationService.cs b/src/Language/Impl/StandardClassification/StandardClassificationService.cs
new file mode 100644
index 0000000..893ad2f
--- /dev/null
+++ b/src/Language/Impl/StandardClassification/StandardClassificationService.cs
@@ -0,0 +1,335 @@
+//
+// 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.Language.StandardClassification.Implementation
+{
+ using Microsoft.VisualStudio.Text.Classification;
+ using Microsoft.VisualStudio.Utilities;
+ using System.ComponentModel.Composition;
+
+ /// <summary>
+ /// Helper service to get hold of standard classifications.
+ /// </summary>
+ [Export(typeof(IStandardClassificationService))]
+ internal sealed class StandardClassificationService : IStandardClassificationService
+ {
+ // =========== Classification types ================
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.NaturalLanguage)]
+ internal ClassificationTypeDefinition naturalLanguageClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition formalLanguageClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Comment)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition commentClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Identifier)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition identifierClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Keyword)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition keywordClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.WhiteSpace)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition whitespaceClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Operator)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition operatorClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Literal)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition literalClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.MarkupAttribute)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition markupAttributeClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.MarkupAttributeValue)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition markupAttributeValueClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.MarkupNode)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition markupNodeClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.String)]
+ [BaseDefinition(PredefinedClassificationTypeNames.Literal)]
+ internal ClassificationTypeDefinition stringClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Type)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition typeClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Character)]
+ [BaseDefinition(PredefinedClassificationTypeNames.Literal)]
+ internal ClassificationTypeDefinition characterClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Number)]
+ [BaseDefinition(PredefinedClassificationTypeNames.Literal)]
+ internal ClassificationTypeDefinition numberClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.Other)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition otherClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.ExcludedCode)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition excludedCodeClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.PreprocessorKeyword)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition preprocessorKeywordClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.SymbolDefinition)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition symbolDefinitionClassificationTypeDefinition;
+
+ [Export]
+ [Name(PredefinedClassificationTypeNames.SymbolReference)]
+ [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)]
+ internal ClassificationTypeDefinition symbolReferenceClassificationTypeDefinition;
+
+ [Import]
+ internal IClassificationTypeRegistryService ClassificationTypeRegistry;
+
+ #region IStandardClassificationService Members
+
+ public IClassificationType CharacterLiteral
+ {
+ get
+ {
+ if (this.characterLiteral == null)
+ {
+ this.characterLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Character);
+ }
+ return this.characterLiteral;
+ }
+ }
+ IClassificationType characterLiteral;
+
+ public IClassificationType Comment
+ {
+ get
+ {
+ if (this.comment == null)
+ {
+ this.comment = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Comment);
+ }
+ return this.comment;
+ }
+ }
+ IClassificationType comment;
+
+ public IClassificationType FormalLanguage
+ {
+ get
+ {
+ if (this.formalLanguage == null)
+ {
+ this.formalLanguage = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.FormalLanguage);
+ }
+ return this.formalLanguage;
+ }
+ }
+ IClassificationType formalLanguage;
+
+ public IClassificationType Identifier
+ {
+ get
+ {
+ if (this.identifier == null)
+ {
+ this.identifier = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Identifier);
+ }
+ return this.identifier;
+ }
+ }
+ IClassificationType identifier;
+
+ public IClassificationType Keyword
+ {
+ get
+ {
+ if (this.keyword == null)
+ {
+ this.keyword = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Keyword);
+ }
+ return this.keyword;
+ }
+ }
+ IClassificationType keyword;
+
+ public IClassificationType Literal
+ {
+ get
+ {
+ if (this.literal == null)
+ {
+ this.literal = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Literal);
+ }
+ return this.literal;
+ }
+ }
+ IClassificationType literal;
+
+ public IClassificationType NaturalLanguage
+ {
+ get
+ {
+ if (this.naturalLanguage == null)
+ {
+ this.naturalLanguage = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.NaturalLanguage);
+ }
+ return this.naturalLanguage;
+ }
+ }
+ IClassificationType naturalLanguage;
+
+ public IClassificationType NumberLiteral
+ {
+ get
+ {
+ if (this.numericalLiteral == null)
+ {
+ this.numericalLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Number);
+ }
+ return this.numericalLiteral;
+ }
+ }
+ IClassificationType numericalLiteral;
+
+ public IClassificationType Operator
+ {
+ get
+ {
+ if (this.@operator == null)
+ {
+ this.@operator = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Operator);
+ }
+ return this.@operator;
+ }
+ }
+ IClassificationType @operator;
+
+ public IClassificationType StringLiteral
+ {
+ get
+ {
+ if (this.stringLiteral == null)
+ {
+ this.stringLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.String);
+ }
+ return this.stringLiteral;
+ }
+ }
+ IClassificationType stringLiteral;
+
+ public IClassificationType WhiteSpace
+ {
+ get
+ {
+ if (this.whiteSpace == null)
+ {
+ this.whiteSpace = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.WhiteSpace);
+ }
+ return this.whiteSpace;
+ }
+ }
+ IClassificationType whiteSpace;
+
+ public IClassificationType Other
+ {
+ get
+ {
+ if (this.other == null)
+ {
+ this.other = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Other);
+ }
+ return this.other;
+ }
+ }
+ IClassificationType other;
+
+ public IClassificationType PreprocessorKeyword
+ {
+ get
+ {
+ if (this.preprocessorKeyword == null)
+ {
+ this.preprocessorKeyword = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.PreprocessorKeyword);
+ }
+ return this.preprocessorKeyword;
+ }
+ }
+ IClassificationType preprocessorKeyword;
+
+ public IClassificationType ExcludedCode
+ {
+ get
+ {
+ if (this.excludedCode == null)
+ {
+ this.excludedCode = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.ExcludedCode);
+ }
+ return this.excludedCode;
+ }
+ }
+ IClassificationType excludedCode;
+
+ public IClassificationType SymbolDefinition
+ {
+ get
+ {
+ if (this.symbolDefinition == null)
+ {
+ this.symbolDefinition = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.SymbolDefinition);
+ }
+ return this.symbolDefinition;
+ }
+ }
+ IClassificationType symbolDefinition;
+
+ public IClassificationType SymbolReference
+ {
+ get
+ {
+ if (this.symbolReference == null)
+ {
+ this.symbolReference = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.SymbolReference);
+ }
+ return this.symbolReference;
+ }
+ }
+ IClassificationType symbolReference;
+
+ #endregion
+ }
+}
diff --git a/src/Language/Impl/StandardClassification/Strings.Designer.cs b/src/Language/Impl/StandardClassification/Strings.Designer.cs
new file mode 100644
index 0000000..f14fe7f
--- /dev/null
+++ b/src/Language/Impl/StandardClassification/Strings.Designer.cs
@@ -0,0 +1,270 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Language.StandardClassification.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Language.StandardClassification.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Comment.
+ /// </summary>
+ internal static string EditorFormat_Comment {
+ get {
+ return ResourceManager.GetString("EditorFormat_Comment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Excluded Code.
+ /// </summary>
+ internal static string EditorFormat_ExcludedCode {
+ get {
+ return ResourceManager.GetString("EditorFormat_ExcludedCode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Identifier.
+ /// </summary>
+ internal static string EditorFormat_Identifier {
+ get {
+ return ResourceManager.GetString("EditorFormat_Identifier", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Keyword.
+ /// </summary>
+ internal static string EditorFormat_Keyword {
+ get {
+ return ResourceManager.GetString("EditorFormat_Keyword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Literal.
+ /// </summary>
+ internal static string EditorFormat_Literal {
+ get {
+ return ResourceManager.GetString("EditorFormat_Literal", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Markup Attribute.
+ /// </summary>
+ internal static string EditorFormat_MarkupAttribute {
+ get {
+ return ResourceManager.GetString("EditorFormat_MarkupAttribute", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Markup Attribute Value.
+ /// </summary>
+ internal static string EditorFormat_MarkupAttributeValue {
+ get {
+ return ResourceManager.GetString("EditorFormat_MarkupAttributeValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Markup Node.
+ /// </summary>
+ internal static string EditorFormat_MarkupNode {
+ get {
+ return ResourceManager.GetString("EditorFormat_MarkupNode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Number.
+ /// </summary>
+ internal static string EditorFormat_Number {
+ get {
+ return ResourceManager.GetString("EditorFormat_Number", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Operator.
+ /// </summary>
+ internal static string EditorFormat_Operator {
+ get {
+ return ResourceManager.GetString("EditorFormat_Operator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Background.
+ /// </summary>
+ internal static string EditorFormat_PeekBackground {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekBackground", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Background Unfocused.
+ /// </summary>
+ internal static string EditorFormat_PeekBackgroundUnfocused {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekBackgroundUnfocused", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Breadcrumb Hovered.
+ /// </summary>
+ internal static string EditorFormat_PeekBreadcrumbHovered {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekBreadcrumbHovered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Breadcrumb Selected.
+ /// </summary>
+ internal static string EditorFormat_PeekBreadcrumbSelected {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekBreadcrumbSelected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Focused Border.
+ /// </summary>
+ internal static string EditorFormat_PeekFocusedBorder {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekFocusedBorder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Highlighted Text.
+ /// </summary>
+ internal static string EditorFormat_PeekHighlightedText {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekHighlightedText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Highlighted Text Unfocused.
+ /// </summary>
+ internal static string EditorFormat_PeekHighlightedTextUnfocused {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekHighlightedTextUnfocused", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Peek Label Text.
+ /// </summary>
+ internal static string EditorFormat_PeekLabelText {
+ get {
+ return ResourceManager.GetString("EditorFormat_PeekLabelText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Preprocessor Keyword.
+ /// </summary>
+ internal static string EditorFormat_PreprocessorKeyword {
+ get {
+ return ResourceManager.GetString("EditorFormat_PreprocessorKeyword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to String.
+ /// </summary>
+ internal static string EditorFormat_String {
+ get {
+ return ResourceManager.GetString("EditorFormat_String", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Symbol Definition.
+ /// </summary>
+ internal static string EditorFormat_SymbolDefinition {
+ get {
+ return ResourceManager.GetString("EditorFormat_SymbolDefinition", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Symbol Reference.
+ /// </summary>
+ internal static string EditorFormat_SymbolReference {
+ get {
+ return ResourceManager.GetString("EditorFormat_SymbolReference", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type.
+ /// </summary>
+ internal static string EditorFormat_Type {
+ get {
+ return ResourceManager.GetString("EditorFormat_Type", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Language/Impl/StandardClassification/Strings.resx b/src/Language/Impl/StandardClassification/Strings.resx
new file mode 100644
index 0000000..fd18ef6
--- /dev/null
+++ b/src/Language/Impl/StandardClassification/Strings.resx
@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="EditorFormat_Comment" xml:space="preserve">
+ <value>Comment</value>
+ <comment>The display name of the 'Comment' editor format.</comment>
+ </data>
+ <data name="EditorFormat_ExcludedCode" xml:space="preserve">
+ <value>Excluded Code</value>
+ <comment>The display name of the 'Excluded Code' editor format.</comment>
+ </data>
+ <data name="EditorFormat_Identifier" xml:space="preserve">
+ <value>Identifier</value>
+ <comment>The display name of the 'Identifier' editor format.</comment>
+ </data>
+ <data name="EditorFormat_Keyword" xml:space="preserve">
+ <value>Keyword</value>
+ <comment>The display name of the 'Keyword' editor format.</comment>
+ </data>
+ <data name="EditorFormat_PreprocessorKeyword" xml:space="preserve">
+ <value>Preprocessor Keyword</value>
+ <comment>The display name of the 'Preprocessor Keyword' editor format.</comment>
+ </data>
+ <data name="EditorFormat_Operator" xml:space="preserve">
+ <value>Operator</value>
+ <comment>The display name of the 'Operator' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_Literal" xml:space="preserve">
+ <value>Literal</value>
+ <comment>The display name of the 'Literal' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_String" xml:space="preserve">
+ <value>String</value>
+ <comment>The display name of the 'String' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_Number" xml:space="preserve">
+ <value>Number</value>
+ <comment>The display name of the 'Number' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_SymbolDefinition" xml:space="preserve">
+ <value>Symbol Definition</value>
+ <comment>The display name of the 'Symbol Definition' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_SymbolReference" xml:space="preserve">
+ <value>Symbol Reference</value>
+ <comment>The display name of the 'Symbol Reference' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekBackground" xml:space="preserve">
+ <value>Peek Background</value>
+ <comment>The display name of the 'Peek Background' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekFocusedBorder" xml:space="preserve">
+ <value>Peek Focused Border</value>
+ <comment>The display name of the 'Peek Focused Background' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekBreadcrumbSelected" xml:space="preserve">
+ <value>Peek Breadcrumb Selected</value>
+ <comment>The display name of the 'Peek Breadcrumb Selected' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekBackgroundUnfocused" xml:space="preserve">
+ <value>Peek Background Unfocused</value>
+ <comment>The display name of the 'Peek Background Unfocused' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekBreadcrumbHovered" xml:space="preserve">
+ <value>Peek Breadcrumb Hovered</value>
+ <comment>The display name of the 'Peek Breadcrumb Hovered' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekLabelText" xml:space="preserve">
+ <value>Peek Label Text</value>
+ <comment>The display name of the 'Peek Label Text' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekHighlightedText" xml:space="preserve">
+ <value>Peek Highlighted Text</value>
+ <comment>The display name of the 'Peek Highlighted Text' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_PeekHighlightedTextUnfocused" xml:space="preserve">
+ <value>Peek Highlighted Text Unfocused</value>
+ <comment>The display name of the 'Peek Highlighted Text Unfocused' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_MarkupNode" xml:space="preserve">
+ <value>Markup Node</value>
+ <comment>The display name of the 'Markup Node' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_MarkupAttribute" xml:space="preserve">
+ <value>Markup Attribute</value>
+ <comment>The display name of the 'Markup Attribute' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_MarkupAttributeValue" xml:space="preserve">
+ <value>Markup Attribute Value</value>
+ <comment>The display name of the 'Markup Attribute Value' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors.</comment>
+ </data>
+ <data name="EditorFormat_Type" xml:space="preserve">
+ <value>Type</value>
+ <comment>The display name of the 'Type' editor format. This shows up in Tools-&gt;Options-&gt;Fonts and Colors</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Text.Implementation.csproj b/src/Microsoft.VisualStudio.Text.Implementation.csproj
new file mode 100644
index 0000000..39876ed
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Text.Implementation.csproj
@@ -0,0 +1,32 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net46</TargetFramework>
+ <SignAssembly>true</SignAssembly>
+ <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
+ <DelaySign>false</DelaySign>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Remove="Core\Def\**\*" />
+ <Compile Remove="Language\**" />
+ <Compile Remove="Text\Def\**\*" />
+ <EmbeddedResource Remove="Core\Def\**\*" />
+ <EmbeddedResource Remove="Language\**\*" />
+ <EmbeddedResource Remove="Text\Def\**\*" />
+ <None Remove="Core\Def\**\*" />
+ <None Remove="Language\**\*" />
+ <None Remove="Text\Def\**\*" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
+ <PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.3.1710.203" />
+ <PackageReference Include="Microsoft.VisualStudio.Language.StandardClassification" Version="15.3.1710.203" />
+ <PackageReference Include="Microsoft.VisualStudio.Text.Data" Version="15.3.1710.203" />
+ <PackageReference Include="Microsoft.VisualStudio.Text.Internal" Version="15.3.1710.203" />
+ <PackageReference Include="Microsoft.VisualStudio.Text.Logic" Version="15.3.1710.203" />
+ <PackageReference Include="Microsoft.VisualStudio.Text.UI" Version="15.3.1710.203" />
+ </ItemGroup>
+
+</Project> \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextData/ExtensionMethods.cs b/src/Text/Def/Internal/TextData/ExtensionMethods.cs
new file mode 100644
index 0000000..dd50540
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/ExtensionMethods.cs
@@ -0,0 +1,74 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+namespace Microsoft.VisualStudio.Text
+{
+ public static class ExtensionMethods
+ {
+ /// <summary>
+ /// Find the index of the next non-whitespace character in a line.
+ /// </summary>
+ /// <param name="line">The line to search.</param>
+ /// <param name="startIndex">The index at which to begin the search, relative to the start of the line.</param>
+ /// <returns>The index, relative to the start of the line, of the first non-whitespace character whose index
+ /// is <paramref name="startIndex"/> or greater, or -1 if there are not any non-whitespace characters at that index or greater.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"> if <paramref name="startIndex"/> is negative.</exception>
+ public static int IndexOfNextNonWhiteSpaceCharacter(this ITextSnapshotLine line, int startIndex)
+ {
+ if (startIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException("start");
+ }
+
+ // Take advantage of performant [] operators on the ITextSnapshot
+ ITextSnapshot snapshot = line.Snapshot;
+ int snapshotPos = line.Start.Position;
+ for (int i = startIndex; i < line.Length; i++)
+ {
+ if (!char.IsWhiteSpace(snapshot[snapshotPos + i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>
+ /// Find the index of the previous non-whitespace character in a line.
+ /// </summary>
+ /// <param name="line">The line to search.</param>
+ /// <param name="startIndex">The index at which to begin the search, relative to the start of the line.</param>
+ /// <returns>The index, relative to the start of the line, of the first non-whitespace character whose index
+ /// is <paramref name="startIndex"/> or greater, or -1 if there are not any non-whitespace characters at that index or greater.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"> if <paramref name="startIndex"/> is negative.</exception>
+ public static int IndexOfPreviousNonWhiteSpaceCharacter(this ITextSnapshotLine line, int startIndex)
+ {
+ if (startIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(startIndex));
+ }
+
+ // Take advantage of performant [] operators on the ITextSnapshot
+ ITextSnapshot snapshot = line.Snapshot;
+ int lineStart = line.Start.Position;
+ for (int i = startIndex - 1; i >= 0; i--)
+ {
+ if (!char.IsWhiteSpace(snapshot[lineStart + i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs b/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs
new file mode 100644
index 0000000..b4a0d46
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs
@@ -0,0 +1,22 @@
+//
+// 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.
+//
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.VisualStudio.Text
+{
+ /// <summary>
+ /// Internal tracker for non-joinable tasks. Used to ensure that all pending tasks
+ /// have completed on editor host shutdown.
+ /// </summary>
+ /// <remarks>Methods of this interface can be called on any thread.</remarks>
+ public interface INonJoinableTaskTrackerInternal
+ {
+ void Register(Task task);
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/IObjectTracker.cs b/src/Text/Def/Internal/TextData/IObjectTracker.cs
new file mode 100644
index 0000000..7b4770b
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/IObjectTracker.cs
@@ -0,0 +1,23 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Describes a tracker of in-memory objects.
+ /// </summary>
+ public interface IObjectTracker
+ {
+ /// <summary>
+ /// Begins tracking an object.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Object"/> to track.</param>
+ /// <param name="bucketName">The name of the bucket in which to track this <see cref="System.Object"/> reference.</param>
+ /// <remarks>Object trackers</remarks>
+ void TrackObject(object value, string bucketName);
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs
new file mode 100644
index 0000000..afe6de3
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs
@@ -0,0 +1,60 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Structure
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.UI.Adornments;
+
+ /// <summary>
+ /// Defines the interface for the <see cref="IStructureSpanningTreeManager"/> which
+ /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>.
+ /// </summary>
+ /// <remarks>
+ /// You can obtain an instance of this class via the <see cref="IStructureSpanningTreeService"/>.
+ /// </remarks>
+ public interface IStructureSpanningTreeManager
+ {
+ /// <summary>
+ /// Event that indicates that <see cref="SpanningTreeSnapshot"/> has been updated,
+ /// and that any method calls on this service will now return more up to date results.
+ /// </summary>
+ event EventHandler SpanningTreeChanged;
+
+ /// <summary>
+ /// Gets an immutable instance of the most up to date current code structure.
+ /// </summary>
+ IStructureElement SpanningTreeSnapshot { get; }
+
+ /// <summary>
+ /// Gets an enumerable of <see cref="IStructureElement"/>s that encapsulate the given <see cref="SnapshotPoint"/>.
+ /// </summary>
+ /// <remarks>
+ /// This method is intended as a projection-aware means to obtain language-service provided
+ /// structural context for a location such as the caret position, or a structure guide line.
+ /// </remarks>
+ /// <param name="point">A <see cref="SnapshotPoint"/> indicating the position of interest.</param>
+ /// <returns>
+ /// The elements within which <paramref name="point"/> is nested, in order, from outermost to innermost.
+ /// </returns>
+ IEnumerable<IStructureElement> GetElementsEncapsulatingPoint(SnapshotPoint point);
+
+ /// <summary>
+ /// Gets an enumerable of <see cref="IStructureElement"/>s that intersect with the given
+ /// <see cref="SnapshotSpan"/>.
+ /// </summary>
+ /// <remarks>
+ /// This method is intended as a projection-aware means to obtain language-service provided
+ /// structural context for a span.
+ /// </remarks>
+ /// <param name="spans">The spans to collect elements from.</param>
+ /// <returns>The elements that intersect with the given span.</returns>
+ IEnumerable<IStructureElement> GetElementsIntersectingSpans(NormalizedSnapshotSpanCollection spans);
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs
new file mode 100644
index 0000000..1ad9c28
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs
@@ -0,0 +1,35 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Structure
+{
+ using System;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// Defines the interface for the <see cref="IStructureSpanningTreeService"/> which can be
+ /// used to obtain instances of the <see cref="IStructureSpanningTreeManager"/>, which
+ /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>.
+ /// </summary>
+ /// <remarks>
+ /// This interface is a MEF component part and can be imported with a MEF import attribute.
+ /// <code>
+ /// [Import]
+ /// internal IStructureSpanningTreeService StructureSpanningTreeService { get; }
+ /// </code>
+ /// </remarks>
+ public interface IStructureSpanningTreeService
+ {
+ /// <summary>
+ /// Gets the singleton <see cref="IStructureSpanningTreeManager"/> for the specified view.
+ /// </summary>
+ /// <param name="textView">The view to get the structure manager for.</param>
+ /// <exception cref="InvalidOperationException">Throw if not called from the UI thread.</exception>
+ /// <returns>The singleton instance of <see cref="IStructureSpanningTreeManager"/> for the view.</returns>
+ IStructureSpanningTreeManager GetManager(ITextView textView);
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs b/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs
new file mode 100644
index 0000000..f9789e3
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs
@@ -0,0 +1,58 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.IO;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The factory service for ordinary TextBuffers.
+ /// </summary>
+ /// <remarks><para>This is a MEF Component, and should be imported as follows:
+ /// <code>
+ /// [Import]
+ /// ITextBufferFactoryService factory = null;
+ /// </code>
+ /// </para>
+ /// <para>Any <see cref="ITextBufferFactoryService"/> will be upcastable to an <see cref="ITextBufferFactoryService2"/>.</para>
+ /// </remarks>
+ public interface ITextBufferFactoryService2 : ITextBufferFactoryService
+ {
+ /// <summary>
+ /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it
+ /// with the given text contained in <paramref name="span"/>.
+ /// </summary>
+ /// <param name="span">The initial text to add.</param>
+ /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param>
+ /// <returns>
+ /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>.
+ /// </returns>
+ /// <exception cref="ArgumentNullException">Either <paramref name="span"/> or <paramref name="contentType"/> is null.</exception>
+ ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType);
+
+ /// <summary>
+ /// Creates an <see cref="ITextBuffer"/> with the given <paramref name="contentType"/> and populates it by
+ /// reading data from the specified TextReader.
+ /// </summary>
+ /// <param name="reader">The TextReader from which to read.</param>
+ /// <param name="contentType">The <paramref name="contentType"/> for the text contained in the new <see cref="ITextBuffer"/></param>
+ /// <param name="length">The length of the file backing the text reader, if known; otherwise -1.</param>
+ /// <param name="traceId">An optional identifier used in debug tracing.</param>
+ /// <returns>
+ /// An <see cref="ITextBuffer"/> object with the given TextReader and <paramref name="contentType"/>.
+ /// </returns>
+ /// <exception cref="ArgumentNullException"><paramref name="reader"/> is null.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="contentType"/> is null.</exception>
+ /// <remarks>
+ /// <para>The <paramref name="reader"/> is not closed by this operation.</para>
+ /// <para>The <paramref name="length"/> is used to help select a storage strategy for the text buffer.</para>
+ /// </remarks>
+ ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length = -1, string traceId = "");
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs b/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs
new file mode 100644
index 0000000..8e91757
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs
@@ -0,0 +1,36 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.IO;
+ using System.IO.MemoryMappedFiles;
+
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The factory service for creating <see cref="ITextImage"/>s.
+ /// </summary>
+ /// <remarks>This is a MEF Component, and should be imported as follows:
+ /// <code>
+ /// [Import]
+ /// ITextImageFactoryService factory = null;
+ /// </code>
+ /// </remarks>
+ public interface ITextImageFactoryService2 : ITextImageFactoryService
+ {
+ /// <summary>
+ /// Create an <see cref="ITextImage"/> from a memory mapped file.
+ /// </summary>
+ /// <param name="source">A utf-16 encoded image of the file.</param>
+ /// <returns>An <see cref="ITextImage"/> that is mapped to the contents of <paramref name="source"/>.</returns>
+ /// <remarks>
+ /// <para><paramref name="source"/> must be encoded as utf-16.</para>
+ /// <para>Any <see cref="ITextImage"/> created from <paramref name="source"/> be invalidated if <paramref name="source"/> is either
+ /// modified or disposed of.</para>
+ /// </remarks>
+ ITextImage CreateTextImage(MemoryMappedFile source);
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs
new file mode 100644
index 0000000..64c0c0d
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs
@@ -0,0 +1,80 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// A helper for managing JoinableTasks.
+ /// </summary>
+ public class JoinableTaskHelper
+ {
+ public readonly JoinableTaskContext Context;
+ public readonly JoinableTaskCollection Collection;
+ public readonly JoinableTaskFactory Factory;
+
+ public JoinableTaskHelper(JoinableTaskContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ this.Context = context;
+ this.Collection = context.CreateCollection();
+ this.Factory = context.CreateFactory(this.Collection);
+ }
+
+ public JoinableTask RunOnUIThread(Action action, bool forceTaskSwitch = true)
+ {
+ using (this.Context.SuppressRelevance())
+ {
+ return this.Factory.RunAsync(async delegate
+ {
+ if (forceTaskSwitch && this.Context.IsOnMainThread)
+ {
+ await Task.Yield();
+ }
+
+ await this.Factory.SwitchToMainThreadAsync();
+ action();
+ });
+ }
+ }
+
+ public JoinableTask<T> RunOnUIThread<T>(Func<T> function, bool forceTaskSwitch = true)
+ {
+ using (this.Context.SuppressRelevance())
+ {
+ return this.Factory.RunAsync(async delegate
+ {
+ if (forceTaskSwitch && this.Context.IsOnMainThread)
+ {
+ await Task.Yield();
+ }
+
+ await this.Factory.SwitchToMainThreadAsync();
+ return function();
+ });
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ await this.Collection.JoinTillEmptyAsync();
+ }
+
+ public void Dispose()
+ {
+ this.Context.Factory.Run(async delegate { // Not this.Factory
+ await this.DisposeAsync();
+ });
+ }
+ }
+}
+
diff --git a/src/Text/Def/Internal/TextData/LazyObservableCollection.cs b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs
new file mode 100644
index 0000000..2c4849f
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs
@@ -0,0 +1,680 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ public delegate TWrapper WrapperCreator<TData, TWrapper>(TData underlyingData, int index);
+
+ /// <summary>
+ /// A virtualized data collection that can be used to create wrapper objects on-demand.
+ /// </summary>
+ public class LazyObservableCollection<TData, TWrapper> :
+ IList<TWrapper>,
+ IList,
+ INotifyCollectionChanged,
+ INotifyPropertyChanged,
+ IDisposable
+ where TWrapper : class
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Private Fields
+
+ private bool _disposed = false;
+ private TData _underlyingDataObject;
+ private int _count;
+ private WrapperCreator<TData, TWrapper> _wrapperCreator;
+ private Dictionary<int, TWrapper> _wrapperDictionary = new Dictionary<int, TWrapper>();
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Constructors
+
+ /// <summary>
+ /// Constructs a virtualized list over an underlying data object.
+ /// </summary>
+ /// <param name="underlyingDataObject">The underlying object over which wrappers will be created.</param>
+ /// <param name="dataObjectCount">
+ /// The number of "items" in the underlying object. Also the number of wrappers to be created.
+ /// </param>
+ /// <param name="wrapperCreator">
+ /// A delegate that will create wrapper objects given an index into the underlying object.
+ /// </param>
+ public LazyObservableCollection
+ (
+ TData underlyingDataObject,
+ int dataObjectCount,
+ WrapperCreator<TData, TWrapper> wrapperCreator
+ )
+ {
+ _underlyingDataObject = underlyingDataObject;
+ _count = dataObjectCount;
+ _wrapperCreator = wrapperCreator;
+ this.SubscribeToEvents();
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region IList<TWrapper> Members
+
+ /// <summary>
+ /// Determines the index of a specific item in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param>
+ /// <returns>The index of item if found in the list; otherwise, -1.</returns>
+ public int IndexOf(TWrapper item)
+ {
+ if (_disposed)
+ return -1;
+
+ // We've got a problem here. The caller is asking us to find the index of a wrapper. This could be a wrapper around
+ // any underlying data object. We have to assume that the caller is asking about a wrapper that we previously gave
+ // to them.
+ if (_wrapperDictionary.ContainsValue(item))
+ {
+ Dictionary<int, TWrapper>.KeyCollection tempKeys = _wrapperDictionary.Keys;
+
+ foreach (int key in tempKeys)
+ {
+ if (item == _wrapperDictionary[key])
+ {
+ return key;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void Insert(int index, TWrapper item)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void RemoveAt(int index)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Gets the element at the specified index. Although the set accessor is defined, it will throw an
+ /// <see cref="InvalidOperationException"/> when called, as the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns>The element at the specified index.</returns>
+ public TWrapper this[int index]
+ {
+ get
+ {
+ return this.GetWrapper(index);
+ }
+ set
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region IList Members
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public int Add(object value)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> contains a specific value.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Object"/> to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param>
+ /// <returns>
+ /// true if the <see cref="System.Object"/> is found in the <see cref="LazyObservableCollection{TData, TWrapper}"/>; otherwise, false.
+ /// </returns>
+ public bool Contains(object value)
+ {
+ TWrapper wrapperObj = value as TWrapper;
+ if ((value != null) && (wrapperObj == null))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value");
+ }
+
+ return this.Contains(wrapperObj);
+ }
+
+ /// <summary>
+ /// Determines the index of a specific item in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Object"/> to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param>
+ /// <returns>The index of value if found in the list; otherwise, -1.</returns>
+ public int IndexOf(object value)
+ {
+ TWrapper wrapperObj = value as TWrapper;
+ if ((value != null) && (wrapperObj == null))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value");
+ }
+
+ return this.IndexOf(wrapperObj);
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void Insert(int index, object value)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="InvalidOperationException"/> has a fixed size.
+ /// </summary>
+ public bool IsFixedSize
+ {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void Remove(object value)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Gets the element at the specified index. Although the set accessor is defined, it will throw an
+ /// <see cref="InvalidOperationException"/> when called, as the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns>The element at the specified index.</returns>
+ object IList.this[int index]
+ {
+ get
+ {
+ return ((IList<TWrapper>)this)[index];
+ }
+ set
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="LazyObservableCollection{TData, TWrapper}"/> to an <see cref="System.Array"/>, starting at a
+ /// particular <see cref="System.Array"/> index.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional <see cref="System.Array"/> that is the destination of the elements copied from
+ /// the <see cref="LazyObservableCollection{TData, TWrapper}"/>. The <see cref="System.Array"/> must have zero-based indexing.
+ /// </param>
+ /// <param name="index">The zero-based index in array at which copying begins.</param>
+ public void CopyTo(Array array, int index)
+ {
+ if ((array.Length - index) < this.Count)
+ {
+ throw new ArgumentException("Array not big enough", "array");
+ }
+
+ int i = index;
+ foreach (TWrapper wrapper in this)
+ {
+ array.SetValue(wrapper, i);
+ i++;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether access to the <see cref="LazyObservableCollection{TData, TWrapper}"/> is synchronized (thread safe).
+ /// </summary>
+ public bool IsSynchronized
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Gets an object that can be used to synchronize access to the <see cref="LazyObservableCollection{TData, TWrapper}"/>.
+ /// </summary>
+ public object SyncRoot
+ {
+ get
+ {
+ if (_disposed)
+ return null;
+
+ return ((ICollection)_wrapperDictionary).SyncRoot;
+ }
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region ICollection<TWrapper> Members
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void Add(TWrapper item)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public void Clear()
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> contains a specific value.
+ /// </summary>
+ /// <param name="item">The value to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param>
+ /// <returns>
+ /// true if the value is found in the <see cref="LazyObservableCollection{TData, TWrapper}"/>; otherwise, false.
+ /// </returns>
+ public bool Contains(TWrapper item)
+ {
+ if (_disposed)
+ return false;
+
+ return _wrapperDictionary.ContainsValue(item);
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="LazyObservableCollection{TData, TWrapper}"/> to an array, starting at a particular array index.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional array that is the destination of the elements copied from the
+ /// <see cref="LazyObservableCollection{TData, TWrapper}"/>. The array must have zero-based indexing.
+ /// </param>
+ /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
+ public void CopyTo(TWrapper[] array, int arrayIndex)
+ {
+ if (_disposed)
+ return;
+
+ // Before we do the copy, we have to ensure that the entire underlying collection is wrapped. That way, the caller
+ // will get a full copy of the list, not just the items we've realized so-far.
+ this.EnsureEntirelyWrapped();
+ _wrapperDictionary.Values.CopyTo(array, arrayIndex);
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.
+ /// </summary>
+ public int Count
+ {
+ get { return _count; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only.
+ /// </summary>
+ public bool Remove(TWrapper item)
+ {
+ throw new InvalidOperationException("The collection is read-only.");
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region IEnumerable<TWrapper> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.Collections.Generic.IEnumerator{T}"/> that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<TWrapper> GetEnumerator()
+ {
+ return new LazyObservableCollectionEnumerator(this);
+ }
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.Collections.IEnumerator"/> that can be used to iterate through the collection.
+ /// </returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new LazyObservableCollectionEnumerator(this);
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region INotifyCollectionChanged Members
+
+ /// <summary>
+ /// Raised when the set of items in the <see cref="LazyObservableCollection{TData, TWrapper}"/> changes.
+ /// </summary>
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region INotifyPropertyChanged Members
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region IDisposable Members
+
+ /// <summary>
+ /// Disposes and releases all wrappers created. Also releases all references to the underlying object
+ /// </summary>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Protected Surface
+
+ protected virtual void Dispose(bool disposeManaged)
+ {
+ if (disposeManaged)
+ {
+ this.UnsubscribeFromEvents();
+
+ // Call dispose on the wrapper items and clean up the list
+ DisposeItems(_wrapperDictionary);
+
+ _wrapperDictionary = null;
+
+ // Stop holding-on to the wrapper creator delegate
+ _wrapperCreator = null;
+ }
+
+ // Stop holding-on to the underlying collection (might be a native COM object, so do this regardless)
+ _underlyingDataObject = default(TData);
+ _count = 0;
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Public Surface
+
+ /// <summary>
+ /// Gets a wrapper object over the underlying object for the specified index.
+ /// </summary>
+ /// <param name="index">The index for which to obtain a wrapper</param>
+ /// <returns>A valid wrapper object for the specified index.</returns>
+ public TWrapper GetWrapper(int index)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException("LazyObservableCollection");
+
+ TWrapper wrapper;
+ if (_wrapperDictionary.TryGetValue(index, out wrapper))
+ {
+ return wrapper;
+ }
+
+ // We don't yet have a wrapper for this data item. Go ahead and create one.
+ wrapper = _wrapperCreator(_underlyingDataObject, index);
+ _wrapperDictionary[index] = wrapper;
+ return wrapper;
+ }
+
+ /// <summary>
+ /// Sets the underlying data object over which this <see cref="LazyObservableCollection{TData, TWrapper}"/> generates
+ /// wrapper objects.
+ /// </summary>
+ /// <param name="newDataObject">The underlying data object over which to generate wrappers.</param>
+ /// <param name="count">The number of items in the underlying object</param>
+ public void SetUnderlyingDataObject(TData newDataObject, int count)
+ {
+ // If we're subscribed to INotifyCollectionChanged on the old underlying data object, stop listening.
+ this.UnsubscribeFromEvents();
+
+ // Reset the underlying object and listen to the new INotifyCollectionChanged, if it exists.
+ _underlyingDataObject = newDataObject;
+ _count = count;
+ this.SubscribeToEvents();
+
+ // Get rid of all of our old wrappers
+ this.NotifyUnderlyingObjectChanged();
+ }
+
+ /// <summary>
+ /// Notifies the <see cref="LazyObservableCollection{TData, TWrapper}"/> that the underlying object over which the
+ /// <see cref="LazyObservableCollection{TData, TWrapper}"/> is based has changed.
+ /// </summary>
+ /// <remarks>
+ /// When the underlying object changes, the <see cref="LazyObservableCollection{TData, TWrapper}"/> resets its wrapper
+ /// collection and raises its INotifyCollectionChanged.CollectionChanged event. The next time wrappers are requested from
+ /// the <see cref="LazyObservableCollection{TData, TWrapper}"/>, the wrappers will be re-generated.
+ /// </remarks>
+ public void NotifyUnderlyingObjectChanged()
+ {
+ if (_disposed)
+ return;
+
+ // Since the underlying collection changed, we can no longer be sure that our wrappers correspond to the right
+ // underlying data objects. Therefore, we'll reset our collection entirely.
+
+ // 1. Keep track of old collection of items
+ Dictionary<int, TWrapper> oldItems = _wrapperDictionary;
+
+ // 2. Recreate the collection of items and update count
+ IList underlyingList = _underlyingDataObject as IList;
+ if (underlyingList != null)
+ {
+ _count = underlyingList.Count;
+ }
+ // we want the dictionary to start small, as its future size is little related
+ // to its prior size (or the size of the underlying list).
+ _wrapperDictionary = new Dictionary<int, TWrapper>();
+
+ // 3. Notify listeners of the change
+ this.RaiseCollectionChanged();
+
+ // 4. Call dispose on the old wrapper items
+ DisposeItems(oldItems);
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Event Handlers
+
+ private void OnUnderlyingCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ this.NotifyUnderlyingObjectChanged();
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Private Implementation
+
+ private static void DisposeItems(Dictionary<int, TWrapper> collection)
+ {
+ foreach (TWrapper wrapper in collection.Values)
+ {
+ IDisposable disposableWrapper = wrapper as IDisposable;
+ if (disposableWrapper != null)
+ {
+ disposableWrapper.Dispose();
+ }
+ }
+ }
+
+ private void EnsureEntirelyWrapped()
+ {
+ for (int i = 0; i < this.Count; i++)
+ {
+ this.GetWrapper(i);
+ }
+ }
+
+ private void RaiseCollectionChanged()
+ {
+ NotifyCollectionChangedEventHandler tempHandler = this.CollectionChanged;
+ if (tempHandler != null)
+ {
+ tempHandler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ // Also notify consumers of the change to the 'Count' property.
+ this.RaisePropertyChanged("Count");
+ }
+
+ private void RaisePropertyChanged(string propertyName)
+ {
+ PropertyChangedEventHandler tempHandler = this.PropertyChanged;
+ if (tempHandler != null)
+ {
+ tempHandler(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ private void SubscribeToEvents()
+ {
+ INotifyCollectionChanged changingUnderlyingColleciton = _underlyingDataObject as INotifyCollectionChanged;
+ if (changingUnderlyingColleciton != null)
+ {
+ changingUnderlyingColleciton.CollectionChanged += this.OnUnderlyingCollection_CollectionChanged;
+ }
+ }
+
+ private void UnsubscribeFromEvents()
+ {
+ INotifyCollectionChanged changingUnderlyingColleciton = _underlyingDataObject as INotifyCollectionChanged;
+ if (changingUnderlyingColleciton != null)
+ {
+ changingUnderlyingColleciton.CollectionChanged -= this.OnUnderlyingCollection_CollectionChanged;
+ }
+ }
+
+ #endregion
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ #region Nested Classes
+
+ private class LazyObservableCollectionEnumerator : IEnumerator<TWrapper>
+ {
+ private int _currentIndex = -1;
+ private LazyObservableCollection<TData, TWrapper> _collection;
+
+ /// <summary>
+ /// Creates an instance of the <see cref="LazyObservableCollectionEnumerator"/> class.
+ /// </summary>
+ /// <param name="collection"></param>
+ public LazyObservableCollectionEnumerator(LazyObservableCollection<TData, TWrapper> collection)
+ {
+ _collection = collection;
+ }
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ public TWrapper Current
+ {
+ get
+ {
+ if (_currentIndex < 0)
+ {
+ throw new InvalidOperationException("Enumerator position before first element in the collection");
+ }
+ return _collection[_currentIndex];
+ }
+ }
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ object IEnumerator.Current
+ {
+ get
+ {
+ if (_currentIndex < 0)
+ {
+ throw new InvalidOperationException("Enumerator position before first element in the collection");
+ }
+ return _collection[_currentIndex];
+ }
+ }
+
+ /// <summary>
+ /// Releases all references to the collection being iterated.
+ /// </summary>
+ public void Dispose()
+ {
+ _collection = null;
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Advances the enumerator to the next element of the collection.
+ /// </summary>
+ /// <returns>
+ /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of
+ /// the collection.
+ /// </returns>
+ public bool MoveNext()
+ {
+ if (_currentIndex < _collection.Count - 1)
+ {
+ _currentIndex++;
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the enumerator to its initial position, which is before the first element in the collection.
+ /// </summary>
+ public void Reset()
+ {
+ _currentIndex = -1;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs
new file mode 100644
index 0000000..4cb900c
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs
@@ -0,0 +1,95 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+namespace Microsoft.VisualStudio.Text
+{
+ public class TextBufferOperationHelpers
+ {
+ /// <summary>
+ /// Checks if the given <see cref="ITextSnapshotLine"/> has any non-whitespace characters
+ /// </summary>
+ /// <param name="line">The <see cref="ITextSnapshotLine"/> on which the check is performed</param>
+ /// <returns>True if the <see cref="ITextSnapshotLine"/> contains any non-whitespace characters</returns>
+ public static bool HasAnyNonWhitespaceCharacters(ITextSnapshotLine line)
+ {
+ return line.IndexOfPreviousNonWhiteSpaceCharacter(line.End.Position - line.Start.Position ) != -1;
+ }
+
+ /// <summary>
+ /// For a given <see cref="ITextSnapshotLine"/> gets the new line character to be inserted to the line based on
+ /// either the given line, or the second last line or the default new line charcter provided by <see cref="IEditorOptions"/>
+ /// </summary>
+ /// <param name="line">The <see cref="ITextSnapshotLine"/> for whcih the new line character is to be decied for</param>
+ /// <param name="editorOptions">The current set of <see cref="IEditorOptions"/> applicable for the given <see cref="ITextSnapshotLine"/></param>
+ /// <returns>The new line character to be inserted</returns>
+ public static string GetNewLineCharacterToInsert(ITextSnapshotLine line, IEditorOptions editorOptions)
+ {
+ string lineBreak = null;
+ var snapshot = line.Snapshot;
+
+ if (editorOptions.GetReplicateNewLineCharacter())
+ {
+ if (line.LineBreakLength > 0)
+ {
+ // use the same line ending as the current line
+ lineBreak = line.GetLineBreakText();
+ }
+ else
+ {
+ if (snapshot.LineCount > 1)
+ {
+ // use the same line ending as the penultimate line in the buffer
+ lineBreak = snapshot.GetLineFromLineNumber(snapshot.LineCount - 2).GetLineBreakText();
+ }
+ }
+ }
+ string textToInsert = lineBreak ?? editorOptions.GetNewLineCharacter();
+ return textToInsert;
+ }
+
+ /// <summary>
+ /// Inserts a final new line for the given <see cref="ITextBuffer"/> based on
+ /// whether the option to insert it is enabled in the current set of <see cref="IEditorOptions"/> applicable to the buffer
+ /// </summary>
+ /// <param name="buffer">The <see cref="ITextBuffer"/> in which the final new line has to be inserted in</param>
+ /// <param name="editorOptions">The current set of <see cref="IEditorOptions"/> applicable to the buffer</param>
+ /// <returns>Whether the operation on the buffer succeded or not</returns>
+ public static bool TryInsertFinalNewLine(ITextBuffer buffer, IEditorOptions editorOptions)
+ {
+ var currentSnapshot = buffer.CurrentSnapshot;
+ var lineCount = currentSnapshot.LineCount;
+ var lastLine = currentSnapshot.GetLineFromLineNumber(lineCount - 1);
+ ITextSnapshot changedSnapshot = null;
+
+ if (lastLine.Start.Position != lastLine.EndIncludingLineBreak.Position) // Check if final new line is not already present
+ {
+ var IsTrimTrailingWhitespacesSetExplicitlyToFalse = editorOptions.IsOptionDefined<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId, true) && !editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId);
+
+ if (!IsTrimTrailingWhitespacesSetExplicitlyToFalse && !HasAnyNonWhitespaceCharacters(lastLine)) // Last Line contains only of whitespace and trim trailing whitespaces is set to false
+ {
+ var spanToDelete = lastLine.ExtentIncludingLineBreak;
+ changedSnapshot = buffer.Delete(spanToDelete);
+ }
+ else // Non empty last line or empty last line with trim trailing whitespaces set to false. Insert a new line after the current line
+ {
+ string lineBreakToInsert = GetNewLineCharacterToInsert(lastLine, editorOptions);
+ var positionToInsertNewLine = lastLine.End.Position;
+ changedSnapshot = buffer.Insert(positionToInsertNewLine, lineBreakToInsert);
+ }
+ // Edits were successfull
+ if (changedSnapshot != null && currentSnapshot != changedSnapshot)
+ return true;
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/TrackingSpanTree.cs b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs
new file mode 100644
index 0000000..8a7b195
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs
@@ -0,0 +1,618 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using Microsoft.VisualStudio.Text;
+
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+
+ /// <summary>
+ /// Holds a well-formed tree of tracking spans and correlated items, for tracking the position and movement
+ /// of spans over time. Allows for efficient addition, removal, and searching over the tree, with methods for
+ /// searching for intersection and containment over both <see cref="SnapshotSpan"/> and <see cref="NormalizedSnapshotSpanCollection"/>
+ /// arguments. The search results are returned pre-order (so parents before children, children closest to the start of the buffer first).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Well-formed means the relationship between all spans is either non-overlapping (siblings) or
+ /// containment (parent-child), so there can be no partially overlapping spans.
+ /// </para>
+ /// <para>
+ /// The only tracking mode that can be safely used is <see cref="SpanTrackingMode.EdgeExclusive"/>, as the other tracking modes
+ /// can result in overlapping spans as the buffer changes.
+ /// </para>
+ /// </remarks>
+ /// <typeparam name="T">The type of object that each span correlates to.</typeparam>
+ /// <comment>This is used by the outlining manager to store collapsed regions and the outlining shims to store all hidden region adapters.</comment>
+ public sealed class TrackingSpanTree<T>
+ {
+ public TrackingSpanNode<T> Root { get; private set; }
+ public ITextBuffer Buffer { get; private set; }
+
+ public int Count { get; private set; }
+
+ private int advanceVersion = 0;
+
+ /// <summary>
+ /// Create a tracking span tree for the given buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer that all the spans in this tree are in.</param>
+ /// <param name="keepTrackingCurrent">The tree should not allow tracking spans to point
+ /// to old versions, at the expense of walking the tree on every text change.</param>
+ public TrackingSpanTree(ITextBuffer buffer, bool keepTrackingCurrent)
+ {
+ if (buffer == null)
+ throw new ArgumentNullException("buffer");
+
+ Buffer = buffer;
+ Count = 0;
+
+ // Leave the tracking span parameter as null, since it is
+ // never used (assumed to always be the entire buffer)
+ Root = new TrackingSpanNode<T>(default(T), null);
+
+ if (keepTrackingCurrent)
+ {
+ buffer.Changed += OnBufferChanged;
+ }
+ }
+
+ /// <summary>
+ /// Try to add an item to the tree with the given tracking span.
+ /// </summary>
+ /// <param name="item">The item to add to the tree.</param>
+ /// <param name="trackingSpan">The tracking span it is associated with.</param>
+ /// <returns>The newly added node, if the item was successfully added; <c>null</c> if adding the item to the tree would
+ /// violate the well-formedness of the tree.</returns>
+ /// <exception cref="ArgumentNullException">If <paramref name="trackingSpan"/> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the tracking mode of <paramref name="trackingSpan"/> is not <see cref="SpanTrackingMode.EdgeExclusive"/>.</exception>
+ public TrackingSpanNode<T> TryAddItem(T item, ITrackingSpan trackingSpan)
+ {
+ if (trackingSpan == null)
+ throw new ArgumentNullException("trackingSpan");
+
+ if (trackingSpan.TrackingMode != SpanTrackingMode.EdgeExclusive)
+ throw new ArgumentException("The tracking mode of the given tracking span must be SpanTrackingMode.EdgeExclusive", "trackingSpan");
+
+ SnapshotSpan spanToAdd = trackingSpan.GetSpan(Buffer.CurrentSnapshot);
+ TrackingSpanNode<T> node = new TrackingSpanNode<T>(item, trackingSpan);
+
+ var newNode = TryAddNodeToRoot(node, spanToAdd, Root);
+ if (newNode != null)
+ Count++;
+
+ return newNode;
+ }
+
+ /// <summary>
+ /// Remove an item from the tree.
+ /// </summary>
+ /// <param name="item">The item to remove.</param>
+ /// <param name="trackingSpan">The span the item was located at.</param>
+ /// <returns><c>true</c> if the item was removed, <c>false</c> if it wasn't found.</returns>
+ public bool RemoveItem(T item, ITrackingSpan trackingSpan)
+ {
+ if (trackingSpan == null)
+ throw new ArgumentNullException("trackingSpan");
+
+ SnapshotSpan spanToRemove = trackingSpan.GetSpan(Buffer.CurrentSnapshot);
+
+ if (RemoveItemFromRoot(item, spanToRemove, Root))
+ {
+ Count--;
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Remove all nodes from the tree.
+ /// </summary>
+ public void Clear()
+ {
+ Root.Children.Clear();
+ Count = 0;
+ }
+
+ /// <summary>
+ /// Find nodes that intersect the given span.
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>Nodes that intersect the given span.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindNodesIntersecting(SnapshotSpan span)
+ {
+ return FindNodesIntersecting(new NormalizedSnapshotSpanCollection(span));
+ }
+
+ /// <summary>
+ /// Find nodes that intersect the given collection of spans (nodes that intersect more than one of the
+ /// spans are only returned once).
+ /// </summary>
+ /// <param name="spans">The collection of spans to search.</param>
+ /// <returns>Nodes that intersect the given collection of spans.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindNodesIntersecting(NormalizedSnapshotSpanCollection spans)
+ {
+ return FindNodes(spans, Root, recurse: true, contained: false);
+ }
+
+ /// <summary>
+ /// Find nodes that intersect the given span that are at the top level of the tree (have no parent nodes).
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>Nodes that are toplevel and intersect the given span.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesIntersecting(SnapshotSpan span)
+ {
+ return FindTopLevelNodesIntersecting(new NormalizedSnapshotSpanCollection(span));
+ }
+
+ /// <summary>
+ /// Find nodes that intersect the given collection of spans that are at the top level of the tree (have no parent nodes).
+ /// </summary>
+ /// <param name="spans">The collection of spans to search.</param>
+ /// <returns>Nodes that are toplevel and intersect the given collection of spans.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesIntersecting(NormalizedSnapshotSpanCollection spans)
+ {
+ return FindNodes(spans, Root, recurse: false, contained: false);
+ }
+
+ /// <summary>
+ /// Find nodes that are contained completely by the given span.
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>Nodes that are contained completely by the given span.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindNodesContainedBy(SnapshotSpan span)
+ {
+ return FindNodesContainedBy(new NormalizedSnapshotSpanCollection(span));
+ }
+
+ /// <summary>
+ /// Find nodes that are contained completely by the given collection of spans.
+ /// </summary>
+ /// <param name="spans">The collection of spans to search.</param>
+ /// <returns>Nodes that are contained completely by the given collection of spans.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindNodesContainedBy(NormalizedSnapshotSpanCollection spans)
+ {
+ return FindNodes(spans, Root, recurse: true, contained: true);
+ }
+
+ /// <summary>
+ /// Find nodes that are contained completely by the given span that are at the top level of the tree (have no parent nodes).
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>Nodes that are toplevel and are contained completely by the given span.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesContainedBy(SnapshotSpan span)
+ {
+ return FindTopLevelNodesContainedBy(new NormalizedSnapshotSpanCollection(span));
+ }
+
+ /// <summary>
+ /// Find nodes that are contained completely by the given collection of spans that are at the top level of the tree (have no parent nodes).
+ /// </summary>
+ /// <param name="spans">The collection of spans to search.</param>
+ /// <returns>Nodes that are toplevel and are contained completely by the given collection of spans.</returns>
+ public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesContainedBy(NormalizedSnapshotSpanCollection spans)
+ {
+ return FindNodes(spans, Root, recurse: false, contained: true);
+ }
+
+ /// <summary>
+ /// Check if a given point is contained inside of a node (so the point is between the start and end points of a node).
+ /// </summary>
+ /// <param name="point">The point to check.</param>
+ /// <returns><c>true</c> if the poin is inside a node.</returns>
+ public bool IsPointContainedInANode(SnapshotPoint point)
+ {
+ return FindChild(point, Root.Children, left: true).Type == FindResultType.Inner;
+ }
+
+ /// <summary>
+ /// Check if a given node is a toplevel node (has no parents).
+ /// </summary>
+ /// <param name="node">The node to check.</param>
+ /// <returns><c>true</c> if the node has no parent node.</returns>
+ public bool IsNodeTopLevel(TrackingSpanNode<T> node)
+ {
+ return Root.Children.Contains(node);
+ }
+
+ public void Advance(ITextVersion toVersion)
+ {
+ if (toVersion == null)
+ {
+ throw new ArgumentNullException("toVersion");
+ }
+
+ if (toVersion.VersionNumber > this.advanceVersion)
+ {
+ this.advanceVersion = toVersion.VersionNumber;
+ Root.Advance(toVersion);
+ }
+ }
+
+ private void OnBufferChanged(object sender, TextContentChangedEventArgs args)
+ {
+ // this only runs in stress mode. It introduces synchronous processing on every buffer change
+ // to advance all the tracking spans in the tree. This prevents these spans from pinning old
+ // versions and obscuring leaks.
+ this.Advance(args.After.Version);
+ }
+
+ #region Static helpers
+
+ static IEnumerable<TrackingSpanNode<T>> FindNodes(NormalizedSnapshotSpanCollection spans, TrackingSpanNode<T> root, bool recurse = true, bool contained = false)
+ {
+ if (spans == null || spans.Count == 0 || root.Children.Count == 0)
+ yield break;
+
+ int requestIndex = 0;
+ SnapshotSpan currentRequest = spans[requestIndex];
+
+ // Find the first child
+ FindResult findResult = FindChild(currentRequest.Start, root.Children, left: true);
+ int childIndex = findResult.Index;
+
+ if (childIndex >= root.Children.Count)
+ yield break;
+
+ ITextSnapshot snapshot = currentRequest.Snapshot;
+ SnapshotSpan currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot);
+
+ while (requestIndex < spans.Count && childIndex < root.Children.Count)
+ {
+ if (currentRequest.Start > currentChild.End)
+ {
+ // Find the next child
+ childIndex = FindNextChild(root, currentRequest.Start, childIndex);
+
+ if (childIndex < root.Children.Count)
+ currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot);
+ }
+ else if (currentChild.Start > currentRequest.End)
+ {
+ // Skip to the next request
+ if (++requestIndex < spans.Count)
+ currentRequest = spans[requestIndex];
+ }
+ else
+ {
+ // Yield the region then move to the next
+ if (!contained || currentRequest.Contains(currentChild))
+ yield return root.Children[childIndex];
+
+ if (recurse)
+ {
+ foreach (var result in FindNodes(spans, root.Children[childIndex], recurse, contained))
+ yield return result;
+ }
+
+ // Find the next child
+ childIndex = FindNextChild(root, currentRequest.Start, childIndex);
+
+ if (childIndex < root.Children.Count)
+ currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot);
+ }
+ }
+ }
+
+ static int FindNextChild(TrackingSpanNode<T> root, SnapshotPoint point, int currentChildIndex)
+ {
+ // If we're already at the end, there's no need to continue searching
+ if (currentChildIndex == root.Children.Count - 1)
+ return currentChildIndex + 1;
+
+ return FindChild(point, root.Children, left: true, lo: currentChildIndex + 1).Index;
+ }
+
+ static bool RemoveItemFromRoot(T item, SnapshotSpan span, TrackingSpanNode<T> root)
+ {
+ if (root.Children.Count == 0)
+ return false;
+
+ var result = FindChild(span.Start, root.Children, left: true);
+
+ if (result.Index < 0 || result.Index >= root.Children.Count)
+ return false;
+
+ // Search from this index onward (there may be empty regions in the way)
+ for (int i = result.Index; i < root.Children.Count; i++)
+ {
+ var child = root.Children[i];
+ SnapshotSpan childSpan = child.TrackingSpan.GetSpan(span.Snapshot);
+
+ // Check to see if we've walked past it
+ if (childSpan.Start > span.End)
+ {
+ return false;
+ }
+ else if (childSpan == span && object.Equals(child.Item, item))
+ {
+ root.Children.RemoveAt(i);
+ root.Children.InsertRange(i, child.Children);
+ return true;
+ }
+ else if (childSpan.Contains(span))
+ {
+ if (RemoveItemFromRoot(item, span, child))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static TrackingSpanNode<T> TryAddNodeToRoot(TrackingSpanNode<T> newNode, SnapshotSpan span, TrackingSpanNode<T> root)
+ {
+ var children = root.Children;
+
+ if (children.Count == 0)
+ {
+ children.Add(newNode);
+ return newNode;
+ }
+
+ FindResult leftResult = FindIndexForAdd(span.Start, children, left: true);
+ FindResult rightResult = FindIndexForAdd(span.End, children, left: false);
+
+ // See if we can add the node anywhere
+
+ // The indices cross if the searches fail, and the node is inside a gap.
+ if (leftResult.Index > rightResult.Index)
+ {
+ // Case #1: If the new node should go in a gap between two nodes, just insert it in the correct location
+ Debug.Assert(leftResult.Type == FindResultType.Outer || rightResult.Type == FindResultType.Outer);
+
+ children.Insert(leftResult.Index, newNode);
+ return newNode;
+ }
+ else
+ {
+ if (leftResult.Type == FindResultType.Inner || rightResult.Type == FindResultType.Inner)
+ {
+ // Case #2: The new node is contained entirely in a single child node, so add it to that child
+ // Check the nodes at either end of the resulting indices (they may not be the same index due to
+ // 0-length nodes that abut the correct child).
+ if (children[leftResult.Index].TrackingSpan.GetSpan(span.Snapshot).Contains(span))
+ return TryAddNodeToRoot(newNode, span, children[leftResult.Index]);
+ if (leftResult.Index != rightResult.Index && children[rightResult.Index].TrackingSpan.GetSpan(span.Snapshot).Contains(span))
+ return TryAddNodeToRoot(newNode, span, children[rightResult.Index]);
+
+ // This fails if the node isn't fully contained in a single child
+ }
+ else
+ {
+ // Case #3: The new node contains any number of children, so we:
+ int start = leftResult.Index;
+ int count = rightResult.Index - leftResult.Index + 1;
+
+ // a) Add all the children this should contain to the new node,
+ newNode.Children.AddRange(children.Skip(start).Take(count));
+ // b) Remove them from the existing root node, and
+ children.RemoveRange(start, count);
+ // c) Add the new node in their place
+ children.Insert(start, newNode);
+
+ return newNode;
+ }
+ }
+
+ // We couldn't find a place to add this node, so return failure
+ return null;
+ }
+
+
+ // Find the child that intersects the given point (or child gap, if no nodes intersect).
+ // When left is true, finds the left-most child intersecting the point. Otherwise, finds the right-most child intersecting the point.
+ static FindResult FindChild(SnapshotPoint point, List<TrackingSpanNode<T>> nodes, bool left, int lo = -1, int hi = -1)
+ {
+ if (nodes.Count == 0)
+ return new FindResult() { Index = 0, Intersects = false, Type = FindResultType.Outer };
+
+ ITextSnapshot snapshot = point.Snapshot;
+ int position = point.Position;
+
+ FindResultType type = FindResultType.Outer;
+
+ bool intersects = false;
+
+ // Binary search for the node containing the position
+ if (lo == -1)
+ lo = 0;
+ if (hi == -1)
+ hi = nodes.Count - 1;
+
+ int mid = lo;
+
+ SnapshotSpan midSpan = new SnapshotSpan();
+ while (lo <= hi)
+ {
+ mid = (lo + hi) / 2;
+
+ midSpan = nodes[mid].TrackingSpan.GetSpan(snapshot);
+
+ if (position < midSpan.Start)
+ {
+ hi = mid - 1;
+ }
+ else if (position > midSpan.End)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ // midSpan contains or abuts the position
+ if (position > midSpan.Start && position < midSpan.End)
+ type = FindResultType.Inner;
+
+ intersects = true;
+
+ break;
+ }
+ }
+
+ int index = mid;
+ midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+
+ // If this is an intersection, make sure we walk to the left or right as requested.
+ if (intersects)
+ {
+ if (left)
+ {
+ while (index >= lo)
+ {
+ midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+ if (position > midSpan.End)
+ {
+ index++;
+ break;
+ }
+
+ index--;
+ }
+
+ // If we fell off the end, just return lo
+ if (index < lo)
+ index = lo;
+ }
+ else
+ {
+ while (index <= hi)
+ {
+ midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+ if (position < midSpan.Start)
+ {
+ index--;
+ break;
+ }
+
+ index++;
+ }
+
+ // If we fell off the end, just return hi
+ if (index > hi)
+ index = hi;
+ }
+ }
+
+ return new FindResult() { Type = type, Index = index, Intersects = intersects};
+ }
+
+ // Find the left/right indices for adding a new node in the scope of the given list of nodes.
+ // When left is true (and the point is not inside a node), finds the leftmost node with a start and end point >= the position.
+ // When left is false (and the point is not inside a node), finds the rightmost node with a start and end point <= the position.
+ static FindResult FindIndexForAdd(SnapshotPoint point, List<TrackingSpanNode<T>> nodes, bool left, int lo = -1, int hi = -1)
+ {
+ ITextSnapshot snapshot = point.Snapshot;
+ int position = point.Position;
+
+ if (lo == -1)
+ lo = 0;
+ if (hi == -1)
+ hi = nodes.Count - 1;
+
+ // Use the general FindIndex to start
+ FindResult result = FindChild(point, nodes, left, lo, hi);
+
+ int index = result.Index;
+ SnapshotSpan midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+
+ // If we hit a gap, figure out the correct index to use
+ if (!result.Intersects)
+ {
+ // midSpan is the last span we found
+ if (position < midSpan.Start && !left)
+ index--;
+ else if (position > midSpan.End && left)
+ index++;
+ }
+ else if (result.Type == FindResultType.Outer)
+ {
+ // For an outer hit, we need to walk left or right to make sure we're containing all empty regions
+ if (left)
+ {
+ while (index <= hi)
+ {
+ midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+ if (position <= midSpan.Start)
+ {
+ break;
+ }
+
+ index++;
+ }
+ }
+ else
+ {
+ while (index >= lo)
+ {
+ midSpan = nodes[index].TrackingSpan.GetSpan(snapshot);
+ if (position >= midSpan.End)
+ {
+ break;
+ }
+
+ index--;
+ }
+ }
+ }
+
+ return new FindResult() { Type = result.Type, Index = index, Intersects = result.Intersects };
+ }
+
+ enum FindResultType
+ {
+ Inner, // The result found is inside a node
+ Outer // The result found is outside of nodes
+ }
+
+ struct FindResult
+ {
+ // Inner or outer, depending on where the result is
+ public FindResultType Type;
+ // The child index of the result
+ public int Index;
+ // true when the result intersects the given index
+ public bool Intersects;
+ }
+ #endregion
+ }
+
+ /// <summary>
+ /// A node in the tracking span tree. A node contains a data item, an associated tracking span,
+ /// and a (possibly empty) list of children, which can be modified as items are inserted and removed into the tree.
+ /// </summary>
+ public sealed class TrackingSpanNode<T>
+ {
+ public TrackingSpanNode(T item, ITrackingSpan trackingSpan) : this(item, trackingSpan, new List<TrackingSpanNode<T>>()) { }
+
+ public TrackingSpanNode(T item, ITrackingSpan trackingSpan, List<TrackingSpanNode<T>> children)
+ {
+ Item = item;
+ TrackingSpan = trackingSpan;
+ Children = children;
+ }
+
+ public T Item { get; private set; }
+ public ITrackingSpan TrackingSpan { get; private set; }
+ public List<TrackingSpanNode<T>> Children { get; private set; }
+
+ internal void Advance(ITextVersion toVersion)
+ {
+ if (TrackingSpan != null)
+ TrackingSpan.GetSpan(toVersion);
+
+ foreach (var child in Children)
+ {
+ child.Advance(toVersion);
+ }
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs
new file mode 100644
index 0000000..6ed9e0a
--- /dev/null
+++ b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs
@@ -0,0 +1,691 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Globalization;
+ using Microsoft.VisualStudio.Text;
+
+ /// <summary>
+ /// This is a little buffer over the contents of a line, so that we avoid fetching the entirety of giant lines when we really
+ /// just need to look at a small local area.
+ /// </summary>
+ public class LineBuffer
+ {
+ public static int BufferSize = 1024; // this is non-constant so unit tests can change it to better test LineBuffer logic
+
+ private readonly ITextSnapshotLine line;
+
+ /// <summary>
+ /// Current window into the line. Size is less than or equal to BufferSize.
+ /// </summary>
+ private string contents;
+
+ /// <summary>
+ /// Bounds of the current contents, 0-origin with respect to the line.
+ /// </summary>
+ private Span extent;
+
+ public LineBuffer(ITextSnapshotLine line)
+ {
+ this.line = line;
+ if (line.LengthIncludingLineBreak <= BufferSize)
+ {
+ this.contents = line.GetTextIncludingLineBreak();
+ this.extent = new Span(0, line.LengthIncludingLineBreak);
+ }
+ else
+ {
+ this.extent = new Span(0, 0);
+ }
+ }
+
+ public char this[int index]
+ {
+ get
+ {
+ if (!this.extent.Contains(index))
+ {
+ // shift the window so that it is centered on "index"
+ int start = Math.Max(0, index - BufferSize / 2);
+ int end = Math.Min(line.LengthIncludingLineBreak, start + BufferSize);
+ this.extent = Span.FromBounds(start, end);
+ this.contents = line.Snapshot.GetText(line.Start + start, this.extent.Length);
+ return this.contents[index - this.extent.Start];
+ }
+ return this.contents[index - this.extent.Start];
+ }
+ }
+ }
+
+ public class UnicodeWordExtent
+ {
+ static public bool FindCurrentToken(SnapshotPoint currentPosition, out SnapshotSpan span)
+ {
+ ITextSnapshotLine textSnapshotLine = currentPosition.GetContainingLine();
+ LineBuffer lineText = new LineBuffer(textSnapshotLine);
+ int lineLength = textSnapshotLine.LengthIncludingLineBreak;
+ int iCol = (currentPosition - textSnapshotLine.Start);
+
+ // handle end of buffer case
+ if (iCol >= lineLength)
+ {
+ span = new SnapshotSpan(currentPosition.Snapshot, textSnapshotLine.End, 0);
+ return true;
+ }
+
+ // scan left for base char
+ while ((iCol > 0) && !IsGraphemeBreak(lineText, iCol))
+ {
+ iCol--;
+ }
+
+ // if it's a word, return the word
+ char ch = lineText[iCol];
+ if (IsWordChar(ch))
+ {
+ return FindCurrentWordCoords(new SnapshotPoint(currentPosition.Snapshot, textSnapshotLine.Start + iCol), out span);
+ }
+
+ // contiguous whitespace
+ int iBeg;
+ if (Char.IsWhiteSpace(ch))
+ {
+ for (iBeg = iCol - 1; iBeg >= 0; --iBeg)
+ {
+ if (!Char.IsWhiteSpace(lineText[iBeg]))
+ break;
+ }
+ iBeg++;
+
+ for (++iCol; iCol < lineLength; ++iCol)
+ {
+ if (!Char.IsWhiteSpace(lineText[iCol]))
+ break;
+ }
+ }
+
+ // contiguous punctuation and math symbols
+ else if (Char.IsPunctuation(ch) || IsMathSymbol(ch))
+ {
+ for (iBeg = iCol - 1; iBeg >= 0; --iBeg)
+ {
+ ch = lineText[iBeg];
+ if (!(Char.IsPunctuation(ch) || IsMathSymbol(ch)))
+ break;
+ }
+ iBeg++;
+
+ for (++iCol; iCol < lineLength; ++iCol)
+ {
+ ch = lineText[iCol];
+ if (!(Char.IsPunctuation(ch) || IsMathSymbol(ch)))
+ break;
+ }
+ }
+
+ //Let's get the whole surrogate pair.
+ else if (Char.IsHighSurrogate(ch) && ((iCol + 1) < lineLength) && Char.IsLowSurrogate(lineText[iCol + 1]))
+ {
+ iBeg = iCol;
+ iCol += 2;
+ }
+
+ // any other single char
+ else
+ {
+ iBeg = iCol++;
+ }
+
+ if (iCol > lineLength)
+ {
+ iCol = lineLength;
+ }
+
+ // Done -- fill in the data
+ span = new SnapshotSpan(currentPosition.Snapshot, (textSnapshotLine.Start + iBeg), (iCol - iBeg));
+
+ return true;
+ }
+
+ //---------------------------------------------------------------------------
+ // FindCurrentWordCoords
+ //---------------------------------------------------------------------------
+ public static bool FindCurrentWordCoords(SnapshotPoint currentPosition, out SnapshotSpan span)
+ {
+ span = new SnapshotSpan(currentPosition, 0);
+
+ ITextSnapshotLine textSnapshotLine = currentPosition.GetContainingLine();
+ LineBuffer lineText = new LineBuffer(textSnapshotLine);
+ int lineLength = textSnapshotLine.LengthIncludingLineBreak;
+ int iCol = (currentPosition - textSnapshotLine.Start);
+
+ // scan left for base char
+ while ((iCol > 0) && !IsGraphemeBreak(lineText, iCol))
+ {
+ iCol--;
+ }
+
+ if ((iCol == lineLength) || !IsWordChar(lineText[iCol]))
+ {
+ if (iCol == 0 || !IsWordChar(lineText[iCol - 1]))
+ {
+ return false;
+ }
+ iCol--;
+ }
+
+ // Find the beginning
+ int iBeg;
+ for (iBeg = iCol; iBeg >= 0; --iBeg)
+ {
+ if (IsWordBreak(lineText, iBeg, false))
+ break;
+ }
+
+ // Find the end
+ for (++iCol; iCol < lineLength; ++iCol)
+ {
+ if (IsWordBreak(lineText, iCol, false))
+ break;
+ }
+
+ // Done -- fill in the data
+ span = new SnapshotSpan(currentPosition.Snapshot, (textSnapshotLine.Start + iBeg), (iCol - iBeg));
+
+ return true;
+ }
+
+ public static bool IsWordBreak(LineBuffer line, int iChar, bool fHanWordBreak)
+ {
+ if (iChar <= 0)
+ return true;
+
+ if (!IsGraphemeBreak(line, iChar))
+ return false;
+
+ char cR = line[iChar];
+
+ int iCharPrev = PrevChar(line, iChar);
+ if (iCharPrev < 0)
+ return true;
+ char cL = line[iCharPrev];
+
+ if (IsNonBreaking(cL) || IsNonBreaking(cR))
+ return false;
+
+ return IsPropBreak(cL, cR, fHanWordBreak);
+ }
+
+ public static int PrevChar(LineBuffer line, int iChar)
+ {
+ if (iChar <= 0)
+ return -1;
+
+ for (--iChar; !IsGraphemeBreak(line, iChar); --iChar) ;
+
+ return iChar;
+ }
+
+ // Returns true if iChar is at a grapheme boundary
+ public static bool IsGraphemeBreak(LineBuffer line, int iChar)
+ {
+ if (iChar <= 0)
+ return true;
+
+ char cR = line[iChar];
+ if (cR == 0)
+ return true;
+
+ char cL = line[(iChar - 1)];
+ if (cL == 0)
+ return true;
+
+ // break around line breaks
+ if (IsLineBreak(cL) || IsLineBreak(cR))
+ return true;
+
+ // don't separate a combining char from it's base
+ if (IsCombining(cR))
+ {
+ // A combining character after a quote or whitespace does not combine with the quote or whitespace
+ if (cL != '\'' && cL != '\"' && !Char.IsWhiteSpace(cL) && Char.IsLetter(cL))
+ {
+ return false;
+ }
+
+ }
+
+ // don't break surrogate pairs
+ if (Char.IsHighSurrogate(cL))
+ {
+ return !Char.IsLowSurrogate(cR);
+ }
+
+ HangulJamoType hjL = GetHangulJamoType(cL);
+ HangulJamoType hjR = GetHangulJamoType(cR);
+ if ((hjL != HangulJamoType.Other) && (hjR != HangulJamoType.Other))
+ {
+ switch (hjL)
+ {
+ case HangulJamoType.Lead:
+ return false;
+ case HangulJamoType.Vowel:
+ return (HangulJamoType.Lead == hjR);
+ case HangulJamoType.Trail:
+ return (HangulJamoType.Trail != hjR);
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+
+ // Modified algorithm from:
+ // [U2] 5.13 <Locating Text Element Boundaries> 'Word Boundaries'
+ //
+ // The rule notation here uses "//" for the italic double dagger used in [U2],
+ // and ! for the 'not' symbol.
+ //
+ // We include decimal digits and LOW LINE ('_') in the definition of a 'word'
+ //
+ public static bool IsPropBreak(char cL, char cR, bool fHanWordBreak)
+ {
+ UnicodeCategory R = Char.GetUnicodeCategory(cR);
+ if (IsPropCombining(R))
+ return false;
+
+ // Rule (1 usually does not occur (no line/para bounds in the text we're searching)
+ // or will be caught by (2 and (3.
+
+ // Rule (1 Para //
+ // Rule (2 !Let // Let
+ // Rule (3 Let // !Let
+ bool fL = IsWordChar(cL);
+ bool fR = IsWordChar(cR);
+ if (fL != fR)
+ return true;
+
+ // break between any two non-word-chars
+ if (!fL && !fR)
+ return true;
+
+ // Break between characters of different scripts (e.g. Arabic/English)
+ // unless one is a letter & the other is a digit
+ UnicodeScript LScript = UScript(cL);
+ UnicodeScript RScript = UScript(cR);
+ if (LScript != RScript)
+ {
+ UnicodeCategory L = Char.GetUnicodeCategory(cL);
+ if ((IsPropAlpha(L) && IsPropAlpha(R)) ||
+ (IsPropDigit(L) && IsPropDigit(R)))
+ return true;
+ }
+
+ if ((LScript == UnicodeScript.CJK) || (RScript == UnicodeScript.CJK))
+ {
+ bool fLHan = IsIdeograph(cL);
+ bool fRHan = IsIdeograph(cR);
+ if (fHanWordBreak && (fLHan || fRHan))
+ return true;
+
+ // Rule (4 !(Hira|Kata|Han) // Hira|Kata|Han
+ if ((LScript != UnicodeScript.CJK) && (RScript == UnicodeScript.CJK))
+ return true;
+
+ // Rule (5 Hira // !Hira
+ bool fRHiragana = IsHiragana(cR);
+ if (IsHiragana(cL) && !fRHiragana)
+ return true;
+
+ // Rule (6 Kata // !(Hira|Kata)
+ if (IsKatakana(cL) && !(fRHiragana || IsKatakana(cR)))
+ return true;
+
+ // Rule (7 Han // !(Hira|Han)
+ if (fLHan && !(fRHan || fRHiragana))
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool IsWordChar(char ch)
+ {
+ return Char.IsLetterOrDigit(ch) || (ch == '_') || (ch == '$');
+ }
+
+ /// <summary>
+ /// Logic from vscommon\unilib\uniword.cpp
+ /// </summary>
+ public static bool IsWholeWord(SnapshotSpan candidate, bool acceptHanWordBreak = true)
+ {
+ ITextSnapshotLine line = candidate.Start.GetContainingLine();
+ LineBuffer lineBuffer = null;
+
+ // A word that spans over two lines is not a whole word
+ if (line.Extent.End < candidate.End)
+ {
+ return false;
+ }
+
+ bool isLeftBreak = candidate.Start == 0;
+ bool isRightBreak = candidate.End == candidate.Snapshot.Length;
+
+ if (!isLeftBreak || !isRightBreak)
+ {
+ lineBuffer = new LineBuffer(line);
+ }
+
+ if (!isLeftBreak)
+ {
+ isLeftBreak = IsWordBreak(lineBuffer, candidate.Start - line.Start, acceptHanWordBreak);
+
+ if (!isLeftBreak)
+ {
+ return false;
+ }
+ }
+
+ if (!isRightBreak)
+ {
+ isRightBreak = IsWordBreak(lineBuffer, candidate.End - line.Start, acceptHanWordBreak);
+ }
+
+ return isLeftBreak && isRightBreak;
+ }
+
+ public static bool IsNonBreaking(char ch)
+ {
+ return ((ch == 0x00a0) || // NO-BREAK SPACE
+ (ch == 0x2011) || // NON-BREAKING HYPHEN
+ (ch == 0x202F) || // NARROW NO-BREAK SPACE
+ (ch == 0x30FC) || // Japanese NO-BREAK character a bit like a long hyphen
+ (ch == 0xfeff)); // ZERO WIDTH NO-BREAK SPACE (byte order mark)
+ }
+
+ public const char UCH_LF = (char)0x000A;
+ public const char UCH_CR = (char)0x000D;
+ public const char UCH_LS = (char)0x2028;
+ public const char UCH_PS = (char)0x2029;
+ public const char UCH_NEL = (char)0x0085;
+ public static bool IsLineBreak(char ch)
+ {
+ return (ch <= UCH_PS) && (UCH_CR == ch || UCH_LF == ch || UCH_LS == ch || UCH_PS == ch || UCH_NEL == ch);
+ }
+
+ public enum UnicodeScript
+ {
+ // Pre-Unicode 3.0 scripts
+
+ NONE = 0x0000, // punctuation, dingbat, symbol, math, ...
+ LATIN = 0x0001,
+ GREEK = 0x0002,
+ CYRILLIC = 0x0003,
+ ARMENIAN = 0x0004,
+ HEBREW = 0x0005,
+ ARABIC = 0x0006,
+ DEVANAGARI = 0x0007,
+ BANGLA = 0x0008,
+ GURMUKHI = 0x0009,
+ GUJARATI = 0x000a,
+ ODIA = 0x000b,
+ TAMIL = 0x000c,
+ TELUGU = 0x000d,
+ KANNADA = 0x000e,
+ MALAYALAM = 0x000f,
+ THAI = 0x0010,
+ LAO = 0x0011,
+ TIBETAN = 0x0012,
+ GEORGIAN = 0x0013,
+ CJK = 0x0014, // Chinese, Japanese, Korean (Han, Katakana, Hiragana, Hangul)
+
+ // Unicode 3.0 added scripts
+
+ BRAILLE = 0x0015,
+ SYRIAC = 0x0016,
+ THAANA = 0x0017,
+ SINHALA = 0x0018,
+ MYANMAR = 0x0019,
+ ETHIOPIC = 0x001a,
+ CHEROKEE = 0x001b,
+ CANADIAN_ABORIGINAL = 0x001c,
+ OGHAM = 0x001d,
+ RUNIC = 0x001e,
+ KHMER = 0x001f,
+ MONGOLIAN = 0x0020,
+ YI = 0x0021
+ }
+
+ public static UnicodeScript UScript(char ch)
+ {
+ if (ch <= 0x024F) return UnicodeScript.LATIN; // 0x0000, 0x007F, Basic Latin
+ // 0x0080, 0x00FF, Latin-1 Supplement
+ // 0x0100, 0x017F, Latin Extended-A
+ // 0x0180, 0x024F, Latin Extended-B
+ if (ch < 0x2000)
+ {
+ if (ch < 0x1000)
+ {
+ if (ch < 0x0370) return UnicodeScript.NONE; // 0x0250, 0x02AF, IPA Extensions
+ // 0x02B0, 0x02FF, Spacing Modifier Letters
+ // 0x0300, 0x036F, Combining Diacritical Marks
+ if (ch < 0x0400) return UnicodeScript.GREEK; // 0x0370, 0x03FF, Greek
+ if (ch <= 0x04FF) return UnicodeScript.CYRILLIC; // 0x0400, 0x04FF, Cyrillic
+ if (ch < 0x0530) return UnicodeScript.NONE; // 0x0500, 0x052F, NONE
+ if (ch < 0x0590) return UnicodeScript.ARMENIAN; // 0x0530, 0x058F, Armenian
+ if (ch < 0x0600) return UnicodeScript.HEBREW; // 0x0590, 0x05FF, Hebrew
+ if (ch < 0x0700) return UnicodeScript.ARABIC; // 0x0600, 0x06FF, ARABIC
+ if (ch <= 0x074F) return UnicodeScript.SYRIAC; // 0x0700, 0x074F, SYRIAC
+ if (ch < 0x0780) return UnicodeScript.NONE; // 0x0750, 0x077F, NONE
+ if (ch <= 0x07BF) return UnicodeScript.THAANA; // 0x0780, 0x07BF, THAANA
+ if (ch < 0x0900) return UnicodeScript.NONE; // 0x07C0, 0x08FF, NONE
+ if (ch < 0x0980) return UnicodeScript.DEVANAGARI; // 0x0900, 0x097F, DEVANAGARI
+ if (ch < 0x0A00) return UnicodeScript.BANGLA; // 0x0980, 0x09FF, BANGLA
+ if (ch < 0x0A80) return UnicodeScript.GURMUKHI; // 0x0A00, 0x0A7F, GURMUKHI
+ if (ch < 0x0B00) return UnicodeScript.GUJARATI; // 0x0A80, 0x0AFF, GUJARATI
+ if (ch < 0x0B80) return UnicodeScript.ODIA; // 0x0B00, 0x0B7F, ODIA
+ if (ch < 0x0C00) return UnicodeScript.TAMIL; // 0x0B80, 0x0BFF, TAMIL
+ if (ch < 0x0C80) return UnicodeScript.TELUGU; // 0x0C00, 0x0C7F, TELUGU
+ if (ch < 0x0D00) return UnicodeScript.KANNADA; // 0x0C80, 0x0CFF, KANNADA
+ if (ch < 0x0D80) return UnicodeScript.MALAYALAM; // 0x0D00, 0x0D7F, MALAYALAM
+ if (ch < 0x0E00) return UnicodeScript.SINHALA; // 0x0D80, 0x0DFF, SINHALA
+ if (ch < 0x0E80) return UnicodeScript.THAI; // 0x0E00, 0x0E7F, THAI
+ if (ch < 0x0F00) return UnicodeScript.LAO; // 0x0E80, 0x0EFF, LAO
+
+ return UnicodeScript.TIBETAN; // 0x0F00, 0x0FFF, TIBETAN
+ }
+ else
+ {
+ if (ch < 0x10A0) return UnicodeScript.MYANMAR; // 0x1000, 0x109F, Myanmar
+ if (ch < 0x1100) return UnicodeScript.GEORGIAN; // 0x10A0, 0x10FF, Georgian
+ if (ch < 0x1200) return UnicodeScript.CJK; // 0x1100, 0x11FF, Hangul Jamo
+ if (ch < 0x13A0) return UnicodeScript.ETHIOPIC; // 0x1200, 0x139F, Ethiopic
+ if (ch < 0x1400) return UnicodeScript.CHEROKEE; // 0x13A0, 0x13FF, Cherokee
+ if (ch < 0x1680) return UnicodeScript.CANADIAN_ABORIGINAL; // 0x1400, 0x167F, Unified Canadian Aboriginal Syllabics
+ if (ch < 0x16A0) return UnicodeScript.OGHAM; // 0x1680, 0x169F, Ogham
+ if (ch < 0x1780) return UnicodeScript.RUNIC; // 0x16A0, 0x177F, Runic
+ if (ch < 0x1800) return UnicodeScript.KHMER; // 0x1780, 0x17FF, Khmer
+ if (ch <= 0x18AF) return UnicodeScript.MONGOLIAN; // 0x1800, 0x18AF, Mongolian
+ if (ch < 0x1E00) return UnicodeScript.NONE; // 0x18B0, 0x1DFF, NONE
+ if (ch < 0x1F00) return UnicodeScript.LATIN; // 0x1E00, 0x1EFF, Latin Extended Additional
+
+ return UnicodeScript.GREEK; // 0x1F00, 0x1FFF, Greek Extended
+ }
+ }
+ if (ch < 0xD800)
+ {
+ if (ch <= 0x27FF) return UnicodeScript.NONE; // 0x2000, 0x206F, General Punctuation
+ // 0x2070, 0x209F, Superscripts and Subscripts
+ // 0x20A0, 0x20CF, Currency Symbols
+ // 0x20D0, 0x20FF, Combining Marks for Symbols
+ // 0x2100, 0x214F, Letterlike Symbols
+ // 0x2150, 0x218F, Number Forms
+ // 0x2190, 0x21FF, Arrows
+ // 0x2200, 0x22FF, Mathematical Operators
+ // 0x2300, 0x23FF, Miscellaneous Technical
+ // 0x2400, 0x243F, Control Pictures
+ // 0x2440, 0x245F, Optical Character Recognition
+ // 0x2460, 0x24FF, Enclosed Alphanumerics
+ // 0x2500, 0x257F, Box Drawing
+ // 0x2580, 0x259F, Block Elements
+ // 0x25A0, 0x25FF, Geometric Shapes
+ // 0x2600, 0x26FF, Miscellaneous Symbols
+ // 0x2700, 0x27BF, Dingbats
+ if (ch <= 0x28FF) return UnicodeScript.BRAILLE; // 0x2800, 0x28FF, Braille Patterns
+ if (ch < 0x2E80) return UnicodeScript.NONE; // 0x2900, 0x2E7F, NONE
+ if (ch <= 0x31BF) return UnicodeScript.CJK; // 0x2E80, 0x2EFF, CJK Radicals Supplement
+ // 0x2F00, 0x2FDF, Kangxi Radicals
+ // 0x2FF0, 0x2FFF, Ideographic Description Characters
+ // 0x3000, 0x303F, CJK Symbols and Punctuation
+ // 0x3040, 0x309F, Hiragana
+ // 0x30A0, 0x30FF, Katakana
+ // 0x3100, 0x312F, Bopomofo
+ // 0x3130, 0x318F, Hangul Compatibility Jamo
+ // 0x3190, 0x319F, Kanbun
+ // 0x31A0, 0x31BF, Bopomofo Extended
+ if (ch < 0x3200) return UnicodeScript.NONE; // 0x31C0, 0x31FF, NONE
+ if (ch <= 0x4DBf) return UnicodeScript.CJK; // 0x3200, 0x32FF, Enclosed CJK Letters and Months
+ // 0x3300, 0x33FF, CJK Compatibility
+ // 0x3400, 0x4DB5, CJK Unified Ideographs Extension A
+ if (ch < 0x4E00) return UnicodeScript.NONE; // 0x4DC0, 0x3DFF, NONE
+ if (ch <= 0x9FFF) return UnicodeScript.CJK; // 0x4E00, 0x9FFF, CJK Unified Ideographs
+ if (ch <= 0xA4CF) return UnicodeScript.YI; // 0xA000, 0xA48F, Yi Syllables
+ // 0xA490, 0xA4CF, Yi Radicals
+ if (ch < 0xAC00) return UnicodeScript.NONE; // 0xA4D0, 0xABFF, NONE
+ if (ch <= 0xD7A3) return UnicodeScript.CJK; // 0xAC00, 0xD7A3, Hangul Syllables
+
+ return UnicodeScript.NONE; // 0xD7A4, 0xD7FF, NONE
+ }
+ if (ch < 0xF900) return UnicodeScript.NONE; // 0xD800, 0xDB7F, High Surrogates
+ // 0xDB80, 0xDBFF, High Private Use Surrogates
+ // 0xDC00, 0xDFFF, Low Surrogates
+ // 0xE000, 0xF8FF, Private Use
+ if (ch < 0xFB00) return UnicodeScript.CJK; // 0xF900, 0xFAFF, CJK Compatibility Ideographs
+ if (ch < 0xFB4F) return UnicodeScript.LATIN; // 0xFB00, 0xFB4F, Alphabetic Presentation Forms
+ if (ch < 0xFE00) return UnicodeScript.ARABIC; // 0xFB50, 0xFDFF, Arabic Presentation Forms-A
+ if (ch < 0xFE30) return UnicodeScript.NONE; // 0xFE20, 0xFE2F, Combining Half Marks
+ if (ch < 0xFE50) return UnicodeScript.CJK; // 0xFE30, 0xFE4F, CJK Compatibility Forms
+ if (ch < 0xFE70) return UnicodeScript.NONE; // 0xFE50, 0xFE6F, Small Form Variants
+ if (ch < 0xFEFF) return UnicodeScript.ARABIC; // 0xFE70, 0xFEFE, Arabic Presentation Forms-B
+ if (ch < 0xFFF0)
+ {
+ if (ch == 0xFEFF) return UnicodeScript.NONE; // 0xFEFF, 0xFEFF, Specials
+
+ // 0xFF00, 0xFF00, Halfwidth and Fullwidth Forms (FALLTHROUGH)
+
+ if ((ch >= 0xFF01) && (ch <= 0xFF5E))
+ return UnicodeScript.LATIN; // 0xFF01, 0xFF5E, LATIN
+
+ return UnicodeScript.CJK; // 0xFF5E, 0xFFEF, Halfwidth and Fullwidth Forms
+ }
+
+ return UnicodeScript.NONE; // 0xFFF0, 0xFFFD, Specials
+ // 0xFFFeE 0xFFFF, NONE
+ }
+
+ //public static bool IsHangul(char ch)
+ //{
+ // bool f = false;
+ // if (ch < 0x1100) {}
+ // else if (ch <= 0x11FF) f = true; // (0x1100 - 0x11FF) Hangul Combining Jamo
+ // else if (ch < 0x3130) {}
+ // else if (ch <= 0x318f) f = true; // (0x3130 - 0x318f) Hangul Compatability Jamo
+ // else if (ch < 0xac00) {}
+ // else if (ch <= 0xd7a3) f = true; // (0xac00 - 0xd7a3) Hangul Syllables
+ // else if (ch < 0xffa0) {}
+ // else if (ch <= 0xffdf) f = true; // (0xffa0 - 0xffdf) Halfwidth Hangul Compatability Jamo
+ // return f;
+ //}
+
+ public static bool IsKatakana(char ch)
+ {
+ // Odd short circuit logic but directly ported from unilib
+ return ((ch >= 0x3031) &&
+ (((ch >= 0x3099) && (ch <= 0x30fe)) ||
+ ((ch >= 0xff66) && (ch <= 0xff9f))));
+ }
+
+ public static bool IsHiragana(char ch)
+ {
+ // Odd short circuit logic but directly ported from unilib
+ return ((ch >= 0x3031) &&
+ (((ch >= 0x3041) && (ch <= 0x309e)) ||
+ (0x30fc == ch) || (0xff70 == ch))); // fullwidth and halfwidth hira/kata prolonged sound mark
+ }
+
+ public static bool IsIdeograph(char ch)
+ {
+ bool f = false;
+ if (ch < 0x4e00) { if (ch == 0x3005) f = true; }
+ else if (ch <= 0x9fa5) f = true;
+ else if (ch < 0xf900) {}
+ else if (ch <= 0xfa2d) f = true;
+ return f;
+ }
+
+ public static bool IsMathSymbol(char ch)
+ {
+ return (Char.GetUnicodeCategory(ch) == UnicodeCategory.MathSymbol);
+ }
+
+ public static bool IsCombining(char ch)
+ {
+ return IsPropCombining(Char.GetUnicodeCategory(ch));
+ }
+
+ public static bool IsPropCombining(UnicodeCategory cat)
+ {
+ return ((cat >= UnicodeCategory.NonSpacingMark) && (cat <= UnicodeCategory.EnclosingMark));
+ }
+
+ public static bool IsPropAlpha(UnicodeCategory cat)
+ {
+ return (cat <= UnicodeCategory.OtherLetter);
+ }
+ public static bool IsPropDigit(UnicodeCategory cat)
+ {
+ return (cat == UnicodeCategory.DecimalDigitNumber);
+ }
+
+ public const char UCH_HANGUL_JAMO_FIRST = (char)0x1100;
+ public const char UCH_HANGUL_JAMO_LEAD_FIRST = (char)0x1100;
+ public const char UCH_HANGUL_JAMO_LEAD_LAST = (char)0x115F;
+ public const char UCH_HANGUL_JAMO_VOWEL_FIRST = (char)0x1160;
+ public const char UCH_HANGUL_JAMO_VOWEL_LAST = (char)0x11A2;
+ public const char UCH_HANGUL_JAMO_TRAIL_FIRST = (char)0x11A8;
+ public const char UCH_HANGUL_JAMO_TRAIL_LAST = (char)0x11F9;
+ public const char UCH_HANGUL_JAMO_LAST = (char)0x11FF;
+ //public static bool IsHangulJamo(char ch)
+ //{
+ // return ((ch >= UCH_HANGUL_JAMO_FIRST) && (ch <= UCH_HANGUL_JAMO_LAST));
+ //}
+
+ public enum HangulJamoType
+ {
+ Other = 0,
+ Lead = 1,
+ Vowel = 2,
+ Trail = 3
+ }
+
+ public static HangulJamoType GetHangulJamoType(char ch)
+ {
+ if (ch < UCH_HANGUL_JAMO_FIRST)
+ return HangulJamoType.Other;
+ if (ch > UCH_HANGUL_JAMO_LAST)
+ return HangulJamoType.Other;
+ if (ch <= UCH_HANGUL_JAMO_LEAD_LAST)
+ return HangulJamoType.Lead;
+ if (ch <= UCH_HANGUL_JAMO_VOWEL_LAST)
+ return HangulJamoType.Vowel;
+ return HangulJamoType.Trail;
+ }
+
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs b/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs
new file mode 100644
index 0000000..b8558d0
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs
@@ -0,0 +1,23 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// Options applicable to Expand and Contract Selection.
+ /// </summary>
+ public static class ExpandContractSelectionOptions
+ {
+ /// <summary>
+ /// The option that determines whether or not expand and contract selection is enable for a particular language.
+ /// </summary>
+ public const string ExpandContractSelectionEnabledOptionId = "ExpandContractSelectionEnabled";
+ public static readonly EditorOptionKey<bool> ExpandContractSelectionEnabledKey = new EditorOptionKey<bool>(ExpandContractSelectionEnabledOptionId);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs b/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs
new file mode 100644
index 0000000..4818da7
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs
@@ -0,0 +1,34 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Classification
+{
+ using System.Collections.Generic;
+ using System.Threading;
+
+ /// <summary>
+ /// Assigns <see cref="IClassificationType"/> objects to the text in a <see cref="ITextBuffer"/>.
+ /// </summary>
+ public interface IAccurateClassifier : IClassifier
+ {
+ /// <summary>
+ /// Gets all the <see cref="ClassificationSpan"/> objects that intersect the given range of text.
+ /// </summary>
+ /// <param name="span">
+ /// The snapshot span.
+ /// </param>
+ /// <returns>
+ /// A list of <see cref="ClassificationSpan"/> objects that intersect with the given range.
+ /// </returns>
+ /// <remarks>
+ /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected
+ /// to return final results (however long it takes to compute) instead of quick but tentative results.</para>
+ /// <para>If the underlying tagger does not support <see cref="IClassifier"/>, then <see cref="IClassifier"/>.GetClassificationSpans(...) is used instead.</para>
+ /// </remarks>
+ IList<ClassificationSpan> GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs b/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs
new file mode 100644
index 0000000..d920b0a
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs
@@ -0,0 +1,76 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using System.Threading;
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ /// <summary>
+ /// Aggregates all the tag providers in a buffer graph for the specified type of tag.
+ /// </summary>
+ /// <typeparam name="T">The type of tag returned by the aggregator.</typeparam>
+ /// <remarks>
+ /// The default tag aggregator implementation also does the following:
+ /// for each <see cref="ITagger&lt;T&gt;"/> over which it aggregates tags, if the tagger is
+ /// <see cref="IDisposable"/>, call Dispose() on it when the aggregator is disposed
+ /// or when the taggers are dropped. For example, you should call Dispose() when
+ /// the content type of a text buffer changes or when a buffer is removed from the buffer graph.
+ /// </remarks>
+ public interface IAccurateTagAggregator<out T> : ITagAggregator<T> where T : ITag
+ {
+ /// <summary>
+ /// Gets all the tags that intersect the specified <paramref name="span"/> of the same type as the aggregator.
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>All the tags that intersect the region.</returns>
+ /// <remarks>
+ /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected
+ /// to return final results (however long it takes to compute) instead of quick but tentative results.</para>
+ /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger&lt;T&gt;"/> objects.
+ /// Because of this, the ordering of the returned mapping spans cannot be predicted.
+ /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped
+ /// to the buffer of interest, into a sortable collection.</para>
+ /// <para>If the underlying tagger does not support <see cref="IAccurateTagger&lt;T&gt;"/>, then <see cref="ITagger&lt;T&gt;"/>.GetTags(...) is used instead.</para>
+ /// </remarks>
+ IEnumerable<IMappingTagSpan<T>> GetAllTags(SnapshotSpan span, CancellationToken cancel);
+
+ /// <summary>
+ /// Gets all the tags that intersect the specified <paramref name="span"/> of the type of the aggregator.
+ /// </summary>
+ /// <param name="span">The span to search.</param>
+ /// <returns>All the tags that intersect the region.</returns>
+ /// <remarks>
+ /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected
+ /// to return final results (however long it takes to compute) instead of quick but tentative results.</para>
+ /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger&lt;T&gt;"/> objects.
+ /// Because of this, the ordering of the returned mapping spans cannot be predicted.
+ /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped
+ /// to the buffer of interest, into a sortable collection.</para>
+ /// <para>If the underlying tagger does not support <see cref="IAccurateTagger&lt;T&gt;"/>, then <see cref="ITagger&lt;T&gt;"/>.GetTags(...) is used instead.</para>
+ /// </remarks>
+ IEnumerable<IMappingTagSpan<T>> GetAllTags(IMappingSpan span, CancellationToken cancel);
+
+ /// <summary>
+ /// Gets all the tags that intersect the specified <paramref name="snapshotSpans"/> of the type of the aggregator.
+ /// </summary>
+ /// <param name="snapshotSpans">The spans to search.</param>
+ /// <returns>All the tags that intersect the region.</returns>
+ /// <remarks>
+ /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected
+ /// to return final results (however long it takes to compute) instead of quick but tentative results.</para>
+ /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger&lt;T&gt;"/> objects.
+ /// Because of this, the ordering of the returned mapping spans cannot be predicted.
+ /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped
+ /// to the buffer of interest, into a sortable collection.</para>
+ /// <para>If the underlying tagger does not support <see cref="IAccurateTagger&lt;T&gt;"/>, then <see cref="ITagger&lt;T&gt;"/>.GetTags(...) is used instead.</para>
+ /// </remarks>
+ IEnumerable<IMappingTagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken cancel);
+
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs b/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs
new file mode 100644
index 0000000..5a30b50
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs
@@ -0,0 +1,33 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System.Threading;
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ /// <summary>
+ /// A provider of tags over a buffer.
+ /// </summary>
+ /// <typeparam name="T">The type of tags to generate.</typeparam>
+ public interface IAccurateTagger<out T> : ITagger<T> where T : ITag
+ {
+ /// <summary>
+ /// Gets all the tags that intersect the <paramref name="spans"/>.
+ /// </summary>
+ /// <param name="spans">The spans to visit.</param>
+ /// <returns>A <see cref="ITagSpan{T}"/> for each tag.</returns>
+ /// <remarks>
+ /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected
+ /// to return final results (however long it takes to compute) instead of quick but tentative results.</para>
+ /// <para>Taggers are not required to return their tags in any specific order.</para>
+ /// <para>The recommended way to implement this method is by using generators ("yield return"),
+ /// which allows lazy evaluation of the entire tagging stack.</para>
+ /// </remarks>
+ IEnumerable<ITagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancel);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IElisionTag.cs b/src/Text/Def/Internal/TextLogic/IElisionTag.cs
new file mode 100644
index 0000000..eaa4502
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IElisionTag.cs
@@ -0,0 +1,26 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ /// <summary>
+ /// Tag indicating spans of text to be excluded from a view.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// IViewTaggerProviders are querried by the editor implementation with this tag type for views having
+ /// the <see cref="F:Microsoft.VisualStudio.Text.Editor.PredefinedTextViewRoles.Structured"/> view role.
+ /// </para>
+ /// <para>
+ /// These tags cause text to be hidden but do not result in any outlining UI.
+ /// IOutliningRegionTags are used to provide data to the outlining manager.
+ /// </para>
+ /// </remarks>
+ public interface IElisionTag : ITag
+ {
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs b/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs
new file mode 100644
index 0000000..f3795c8
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs
@@ -0,0 +1,20 @@
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Service for querying the status of A/B experiments.
+ /// </summary>
+ public interface IExperimentationServiceInternal
+ {
+ /// <summary>
+ /// Checks whether or not the flight is enabled for this user.
+ /// </summary>
+ /// <param name="flightName">A name of a flight, up to 16 characters long.</param>
+ /// <returns>True if this user has the specific flight enabled.</returns>
+ /// <remarks>
+ /// This method uses cached flighting results, meaning, that this method does not
+ /// block to download flight membership data, but rather, returns false if the data
+ /// is not yet available.
+ /// </remarks>
+ bool IsCachedFlightEnabled(string flightName);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
new file mode 100644
index 0000000..e78084b
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
@@ -0,0 +1,89 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+
+using System.Collections.Generic;
+using System;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Allows code in src/Platform to log events.
+ /// </summary>
+ /// <remarks>
+ /// For example, the VS Provider of this inserts data points into the telemetry data stream.
+ /// </remarks>
+ public interface ILoggingServiceInternal
+ {
+ /// <summary>
+ /// Post the event named <paramref name="key"/> to the telemetry stream. Additional properties can be appended as name/value pairs in <paramref name="namesAndProperties"/>.
+ /// </summary>
+ void PostEvent(string key, params object[] namesAndProperties);
+
+ /// <summary>
+ /// Post the event named <paramref name="key"/> to the telemetry stream. Additional properties can be appended as name/value pairs in <paramref name="namesAndProperties"/>.
+ /// </summary>
+ void PostEvent(string key, IReadOnlyList<object> namesAndProperties);
+
+ void PostEvent(
+ TelemetryEventType eventType,
+ string eventName,
+ TelemetryResult result = TelemetryResult.Success,
+ params (string name, object property)[] namesAndProperties);
+
+ void PostEvent(
+ TelemetryEventType eventType,
+ string eventName,
+ TelemetryResult result,
+ IReadOnlyList<(string name, object property)> namesAndProperties);
+
+ /// <summary>
+ /// Creates and posts a FaultEvent.
+ /// </summary>
+ /// <param name="eventName">
+ /// An event name following data model schema.
+ /// It requires that event name is a unique, not null or empty string.
+ /// It consists of 3 parts and must follows pattern [product]/[featureName]/[entityName]. FeatureName could be a one-level feature or feature hierarchy delimited by "/".
+ /// For examples,
+ /// vs/platform/opensolution;
+ /// vs/platform/editor/lightbulb/fixerror;
+ /// </param>
+ /// <param name="description">Fault description</param>
+ /// <param name="exceptionObject">Exception instance</param>
+ /// <param name="additionalErrorInfo">Additional information to be added to Watson's ErrorInformation.txt file.</param>
+ /// <param name="isIncludedInWatsonSample">
+ /// Gets or sets a value indicating whether we sample this event locally. Affects Watson only.
+ /// If false, will not send to Watson: only sends the telemetry event to AI and doesn't call callback.
+ /// Changing this will force the event to send to Watson. Be careful because it can have big perf impact.
+ /// If unchanged, it will be set according to the default sample rate.
+ /// </param>
+ void PostFault(
+ string eventName,
+ string description,
+ Exception exceptionObject,
+ string additionalErrorInfo,
+ bool? isIncludedInWatsonSample);
+
+ /// <summary>
+ /// Adjust the counter associated with <paramref name="key"/> and <paramref name="name"/> by <paramref name="delta"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>Counters start at 0.</para>
+ /// <para>No information is sent over the wire until the <see cref="PostCounters"/> is called.</para>
+ /// </remarks>
+ void AdjustCounter(string key, string name, int delta = 1);
+
+ /// <summary>
+ /// Post all of the counters.
+ /// </summary>
+ /// <remarks>
+ /// <para>The counters are logged as if PostEvent had been called for each key with a list counter names and values.</para>
+ /// <para>The counters are cleared as a side-effect of this call.</para>
+ /// </remarks>
+ void PostCounters();
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs b/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs
new file mode 100644
index 0000000..d13feb5
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs
@@ -0,0 +1,23 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Allows marking actions for performance logging.
+ /// </summary>
+ /// <remarks>
+ /// For example, the VS editor adapters return MeasurementBlock instances
+ /// that log ETW events.
+ /// </remarks>
+ public interface IPerformanceMarkerBlockProvider
+ {
+ IDisposable CreateBlock(string blockName);
+ }
+} \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs b/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs
new file mode 100644
index 0000000..2f1a48a
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs
@@ -0,0 +1,29 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// A service that propagates <see cref="IEditorOptions"/> to the text model component.
+ /// This is never intended to be part of the public API -- we already have the
+ /// editor options facilities for that. This is inteded to allow hosting code (e.g. the
+ /// Visual Studio editor package) to propagate options down to the text model,
+ /// where EditorOptions isn't visible.
+ /// </summary>
+ /// <remarks>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// ITextModelOptionsSetter setter = null;
+ /// </remarks>
+ public interface ITextModelOptionsSetter
+ {
+ /// <summary>
+ /// Extract options useful to the text model layer and expose them in
+ /// that layer.
+ /// </summary>
+ void SetTextModelOptions(IEditorOptions options);
+ }
+} \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs
new file mode 100644
index 0000000..57676ee
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs
@@ -0,0 +1,24 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+ /// <summary>
+ /// Provides a service to navigate between search results on a <see cref="ITextBuffer"/> and to
+ /// perform replacements.
+ /// </summary>
+ public interface ITextSearchNavigator2 : ITextSearchNavigator
+ {
+ /// <summary>
+ /// Indicates the ranges that should be searched (if any).
+ /// </summary>
+ /// <remarks>
+ /// If this value to a non-null value will effectively override the ITextSearchNavigator.SearchSpan property.
+ /// </remarks>
+ NormalizedSnapshotSpanCollection SearchSpans { get; set; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs b/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs
new file mode 100644
index 0000000..0990135
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs
@@ -0,0 +1,21 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ /// <summary>
+ /// Represents search results that are provided by a search tagger.
+ /// </summary>
+ /// <remarks>
+ /// The <see cref="ITextSearchResultTag"/> is present such that all consumers of search matches have a common way of obtaining the matches.
+ /// </remarks>
+ public interface ITextSearchResultTag : ITag
+ {
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs
new file mode 100644
index 0000000..f351d11
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs
@@ -0,0 +1,101 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+ using System;
+
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ /// <summary>
+ /// A tagger that tags contents of a buffer based on the search terms that are passed to the object. To
+ /// obtain an implementation of this interface, import the <see cref="ITextSearchTaggerFactoryService"/>
+ /// via the Managed Extensibility Framework.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// All search operations are performed on a low priority background thread and on demand.
+ /// </para>
+ /// <para>
+ /// In order for this tagger to be consumed by the editor, a corresponding <see cref="ITaggerProvider"/>
+ /// that provides an instance of this tagger must be exported through the Managed Extensibility Framework.
+ /// </para>
+ /// </remarks>
+ /// <example>
+ /// <code>
+ /// [Export]
+ /// [TagType(typeof(T))]
+ /// [ContentType("any")]
+ /// class TaggerProvider : ITaggerProvider
+ /// {
+ /// [Import]
+ /// ITextSearchTaggerFactoryService searchTaggerFactory;
+ ///
+ /// #region ITaggerProvider Members
+ ///
+ /// public ITagger&lt;T&gt; CreateTagger&lt;T&gt;(Microsoft.VisualStudio.Text.ITextBuffer buffer) where T : ITag
+ /// {
+ /// ITextSearchTagger&lt;T&gt; tagger = searchTaggerFactory.CreateTextSearchTagger&lt;T&gt;(buffer);
+ ///
+ /// tagger.TagTerm(...);
+ ///
+ /// return tagger as ITagger&lt;T&gt;;
+ /// }
+ ///
+ /// #endregion
+ /// }
+ /// </code>
+ /// </example>
+ /// <typeparam name="T">
+ /// A derivative of <see cref="ITag"/>.
+ /// </typeparam>
+ /// <remarks>
+ /// The <see cref="ITextSearchTagger{T}"/> expects to be queried for monotonically increasing snapshot versions. If a query
+ /// is made in the reverse order, the results returned by the tagger for older versions might differ from the results
+ /// obtained originally for those versions.
+ /// </remarks>
+ public interface ITextSearchTagger<T> : ITagger<T> where T : ITag
+ {
+ /// <summary>
+ /// Limits the scope of the tagger to the provided <see cref="NormalizedSnapshotSpanCollection"/>.
+ /// </summary>
+ /// <remarks>
+ /// If the value is set to <c>null</c> the entire range of the buffer will be searched.
+ /// </remarks>
+ NormalizedSnapshotSpanCollection SearchSpans { get; set; }
+
+ /// <summary>
+ /// Starts tagging occurences of the <paramref name="searchTerm"/>.
+ /// </summary>
+ /// <param name="searchTerm">
+ /// The term to search for.
+ /// </param>
+ /// <param name="searchOptions">
+ /// The options to use for the search.
+ /// </param>
+ /// <param name="tagFactory">
+ /// A factory delegate used to generate tags for matches. The delegate is passed as input
+ /// a <see cref="SnapshotSpan"/> corresponding to a match and is expected to return the corresponding tag.
+ /// </param>
+ /// <exception cref="ArgumentException">If <paramref name="searchOptions"/> requests the search to be
+ /// performed in the reverse direction (see remarks).</exception>
+ /// <exception cref="ArgumentException">If <paramref name="searchOptions"/> requests the search to be performed with
+ /// wrap (see remarks).</exception>
+ /// <remarks>
+ /// In order to guarantee that the tagger finds all matches in a given span of text, the searches are always
+ /// performed in the forwards direction with no wrap. If the <paramref name="searchOptions"/> passed to the
+ /// tagger indicate otherwise, an exception will be thrown.
+ /// </remarks>
+ void TagTerm(string searchTerm, FindOptions searchOptions, Func<SnapshotSpan, T> tagFactory);
+
+ /// <summary>
+ /// Clears any existing tags and all search terms that are being search for. Cancels any
+ /// ongoing background searches.
+ /// </summary>
+ void ClearTags();
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs b/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs
new file mode 100644
index 0000000..fa677d4
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs
@@ -0,0 +1,39 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ /// <summary>
+ /// Provides <see cref="ITextSearchTagger{T}"/> objects.
+ /// </summary>
+ /// <remarks>
+ /// This class is a Managed Extensibility Framework service provided by the editor.
+ /// </remarks>
+ /// <example>
+ /// [Import]
+ /// ITextSearchTaggerFactoryService TextSearchTaggerProvider { get; set; }
+ /// </example>
+ public interface ITextSearchTaggerFactoryService
+ {
+ /// <summary>
+ /// Creates an <see cref="ITextSearchTagger{T}"/> that searches the <paramref name="buffer"/>.
+ /// </summary>
+ /// <typeparam name="T">
+ /// The type of tags the tagger will produce.
+ /// </typeparam>
+ /// <param name="buffer">
+ /// The <see cref="ITextBuffer"/> the tagger will search.
+ /// </param>
+ /// <returns>
+ /// A <see cref="ITextSearchTagger{T}"/> that searches the contents of <paramref name="buffer"/>.
+ /// </returns>
+ ITextSearchTagger<T> CreateTextSearchTagger<T>(ITextBuffer buffer) where T : ITag;
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs b/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs
new file mode 100644
index 0000000..ab6bc86
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs
@@ -0,0 +1,35 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// An attribute used to indicate the priority of a named MEF import. If two imports have
+ /// the same name, then the one with the higher priority wins,
+ /// </summary>
+ /// <remarks><para>Currently used only by the EditorFormatMap imports.</para>
+ /// <para>Default priority is 0 and negative priorities are allowed.</para></remarks>
+ [global::System.AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
+ public sealed class PriorityAttribute : Attribute
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="PriorityAttribute"/>.
+ /// </summary>
+ public PriorityAttribute(int priority)
+ {
+ this.Priority = priority;
+ }
+
+ /// <summary>
+ /// Gets the priority of the export (where an export with the same name and a lower priority will be
+ /// suppressed by the export with a higher priority).
+ /// </summary>
+ public int Priority { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs
new file mode 100644
index 0000000..77476d3
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs
@@ -0,0 +1,59 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ using System;
+ using Microsoft.VisualStudio.Text.Projection;
+
+ /// <summary>
+ /// Tag Aggregator options.
+ /// </summary>
+ [Flags]
+ public enum TagAggregatorOptions2
+ {
+ /// <summary>
+ /// Default behavior. The tag aggregator will map up and down through all projection buffers.
+ /// </summary>
+ None = TagAggregatorOptions.None,
+
+ /// <summary>
+ /// Only map through projection buffers that have the "projection" content type.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Normally, a tag aggregator will map up and down through all projection buffers (buffers
+ /// that implement <see cref="IProjectionBufferBase"/>). This flag will cause the projection buffer
+ /// to not map through buffers that are projection buffers but do not have a projection content type.
+ /// </para>
+ /// </remarks>
+ /// <comment>This is used by the classifier aggregator, as classification depends on content type.</comment>
+ MapByContentType = TagAggregatorOptions.MapByContentType,
+
+ /// <summary>
+ /// Delay creating the taggers for the tag aggregator.
+ /// </summary>
+ /// <remarks>
+ /// <para>A tag aggregator will, normally, create all of its taggers when it is created. This option
+ /// will cause the tagger to defer the creation until idle time tasks are done.</para>
+ /// <para>If this option is set, a TagsChanged event will be raised after the taggers have been created.</para>
+ /// </remarks>
+ DeferTaggerCreation = 0x02,
+
+ /// <summary>
+ /// Do not create taggers on child buffers.
+ /// </summary>
+ /// <remarks>
+ /// <para>A common reason to use this flag would for a tagger that is creating its own tag aggregator
+ /// (for example, to translate one tag into another type of tag). In that case, you can expect another
+ /// instance of your tagger to be created on the child buffers (which would create its own tag aggregators)
+ /// so you don't want to have your tag aggregator include those buffers/
+ /// </para>
+ /// </remarks>
+ NoProjection = 0x04
+ }
+} \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs b/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs
new file mode 100644
index 0000000..39926de
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs
@@ -0,0 +1,33 @@
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Supported telemetry event types.
+ /// </summary>
+ public enum TelemetryEventType : int
+ {
+ /// <summary>
+ /// User task event
+ /// </summary>
+ UserTask = 0,
+
+ /// <summary>
+ /// Trace event
+ /// </summary>
+ Trace = 1,
+
+ /// <summary>
+ /// Operation event
+ /// </summary>
+ Operation = 2,
+
+ /// <summary>
+ /// Fault event
+ /// </summary>
+ Fault = 3,
+
+ /// <summary>
+ /// Asset event
+ /// </summary>
+ Asset = 4,
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/TelemetryResult.cs b/src/Text/Def/Internal/TextLogic/TelemetryResult.cs
new file mode 100644
index 0000000..4f74041
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/TelemetryResult.cs
@@ -0,0 +1,35 @@
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// An enum to define the result from user task or operation.
+ /// </summary>
+ public enum TelemetryResult : int
+ {
+ /// <summary>
+ /// Used for unknown or unavailable result.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// A result without any failure from product or user.
+ /// </summary>
+ Success = 1,
+
+ /// <summary>
+ /// A result to indicate the action/operation failed because of product issue (not user faults)
+ /// Consider using FaultEvent to provide more details about the failure.
+ /// </summary>
+ Failure = 2,
+
+ /// <summary>
+ /// A result to indicate the action/operation failed because of user fault (e.g., invalid input).
+ /// Consider using FaultEvent to provide more details.
+ /// </summary>
+ UserFault = 3,
+
+ /// <summary>
+ /// A result to indicate the action/operation is cancelled by user.
+ /// </summary>
+ UserCancel = 4
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs b/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs
new file mode 100644
index 0000000..7d446a6
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs
@@ -0,0 +1,38 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Defines the positioning of adornments.
+ /// </summary>
+ /// <remarks>
+ /// This enum adds a mode to the AdornmentPositioningBehavior needed for diff but we don't want to expose.
+ /// </remarks>
+ public enum AdornmentPositioningBehavior2
+ {
+ /// <summary>
+ /// The adornment is not moved automatically.
+ /// </summary>
+ OwnerControlled = AdornmentPositioningBehavior.OwnerControlled,
+
+ /// <summary>
+ /// The adornment is positioned relative to the top left corner of the view.
+ /// </summary>
+ ViewportRelative = AdornmentPositioningBehavior.ViewportRelative,
+
+ /// <summary>
+ /// The adornment is positioned relative to the text in the view.
+ /// </summary>
+ TextRelative = AdornmentPositioningBehavior.TextRelative,
+
+ /// <summary>
+ /// Behaves like a AdornmentPositioningBehavior.TextRelative adornment but only scrolls vertically.
+ /// </summary>
+ TextRelativeVerticalOnly
+ }
+} \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextUI/Caret.cs b/src/Text/Def/Internal/TextUI/Caret.cs
new file mode 100644
index 0000000..e6faab3
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/Caret.cs
@@ -0,0 +1,237 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System;
+
+ /// <summary>
+ /// Manipulates the on-screen caret in the editor.
+ /// </summary>
+ public abstract class Caret : DisplayTextPoint
+ {
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the next character.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToNextCharacter(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the previous character.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToPreviousCharacter(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the beginning of the previous line in the buffer.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToBeginningOfPreviousLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the beginning of the next line in the buffer.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToBeginningOfNextLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the beginning of the previous line in the view.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ /// <remarks>
+ /// If the caret is on the first line of the file, the caret is moved to the beginning of the line.
+ /// </remarks>
+ public abstract void MoveToBeginningOfPreviousViewLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the beginning of the next line in the view.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ /// <remarks>
+ /// If the caret is on the last line of the file, the caret is moved to the end of the line.
+ /// </remarks>
+ public abstract void MoveToBeginningOfNextViewLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one line up, preserving its horizontal position.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToPreviousLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one line down, preserving its horizontal position.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToNextLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one page up.
+ /// </summary>
+ public abstract void MovePageUp();
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one page down.
+ /// </summary>
+ public abstract void MovePageDown();
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one page up.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MovePageUp(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret one page down.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MovePageDown(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the end of the line in the buffer.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToEndOfLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the start of the line in the buffer.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToStartOfLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the end of the line in the view.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToEndOfViewLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the start of the line in the view.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToStartOfViewLine(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the position and optionally extends the selection
+ /// if necessary.
+ /// </summary>
+ /// <param name="position">
+ /// The position to place the caret.
+ /// </param>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than 0 or greater than the line number of the last line in the TextBuffer.</exception>
+ public abstract void MoveTo(int position, bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the start of the specified line.
+ /// </summary>
+ /// <param name="lineNumber">
+ /// The line number to which to move the caret.
+ /// </param>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than 0 or greater than the line number of the last line in the TextBuffer.</exception>
+ public abstract void MoveToLine(int lineNumber, bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to an offset from the start of the specified line.
+ /// </summary>
+ /// <param name="lineNumber">
+ /// The line number to which to move the caret.
+ /// </param>
+ /// <param name="offset">
+ /// The number of characters from the start of the line at which the caret should be moved.
+ /// </param>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ /// <remarks>If <paramref name="offset"/> exceeds the length of the line and virtual space is enabled, the caret will be
+ /// positioned in virtual space. Otherwise the caret will be placed at the end of the line.</remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero
+ /// or greater than the line number of the last line in the text buffer, or
+ /// <paramref name="offset"/> is less than zero.</exception>
+ public abstract void MoveToLine(int lineNumber, int offset, bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the start of the document.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToStartOfDocument(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the end of the document.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToEndOfDocument(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class,m oves the caret to the end of the current word, or to the beginning of the
+ /// next word if it is already at the end of the current word.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToNextWord(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the caret to the start of the current word, or to the end of the
+ /// previous word if it is already at the start of the current word.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended.
+ /// </param>
+ public abstract void MoveToPreviousWord(bool extendSelection);
+
+ /// <summary>
+ /// When implemented in a derived class, ensures that the caret is visible on the screen.
+ /// </summary>
+ public abstract void EnsureVisible();
+
+ /// <summary>
+ /// When implemented in a derived class, gets advanced caret functionality.
+ /// </summary>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract ITextCaret AdvancedCaret
+ {
+ get;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs b/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs
new file mode 100644
index 0000000..74302ea
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs
@@ -0,0 +1,144 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using Microsoft.VisualStudio.Text.Formatting;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Represents a point in the <see cref="TextBuffer"/> that behaves relative to the view in which it lives.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// While this point is immutable, its position may change in response
+ /// to edits in the text.
+ /// </para>
+ /// <para>
+ /// The start point is always before the end point.
+ /// </para>
+ /// </remarks>
+ public abstract class DisplayTextPoint : TextPoint
+ {
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextView"/> of this point.
+ /// </summary>
+ public abstract TextView TextView { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="ITextViewLine"/> that contains this point.
+ /// </summary>
+ public abstract ITextViewLine AdvancedTextViewLine { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the position of the start of the line in the TextView that this DisplayTextPoint is on.
+ /// </summary>
+ /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks>
+ public abstract int StartOfViewLine { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the position of the end of the line in the TextView that this DisplayTextPoint is on.
+ /// </summary>
+ /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks>
+ public abstract int EndOfViewLine { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the end of the line in the TextView that it is currently on.
+ /// </summary>
+ /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks>
+ public abstract void MoveToEndOfViewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the start of the line in the TextView that it is currently on.
+ /// </summary>
+ /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks>
+ public abstract void MoveToStartOfViewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the beginning of the next line in the TextView.
+ /// </summary>
+ /// <remarks>
+ /// <para>This point moves to the end of the line if the point is on the last
+ /// line.</para>
+ /// <para>This value could be affected by whether or not Word Wrap is turned on in the view.</para>
+ /// <para>If the point is on the last line of the file, the caret is moved to the end of the line.</para>
+ /// </remarks>
+ public abstract void MoveToBeginningOfNextViewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the beginning of the previous line in the TextView.
+ /// </summary>
+ /// <remarks>
+ /// <para>This point moves to the start of the line if the point is on the first
+ /// line.</para>
+ /// <para>This value could be affected by whether or not Word Wrap is turned on in the view.</para>
+ /// <para>If the point is on the first line of the file, the caret is moved to the beginning of the line.</para>
+ /// </remarks>
+ public abstract void MoveToBeginningOfPreviousViewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text point for the first
+ /// non-whitespace character on the current view line.
+ /// </summary>
+ /// <remarks>
+ /// If a line is all white space, this method returns a <see cref="DisplayTextPoint"/> at the start of the line.
+ /// </remarks>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the integer representation of the current position of this text point
+ /// in relation to the visual start of the line.
+ /// </summary>
+ public abstract int DisplayColumn { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, determines whether the point is currently visible on the screen.
+ /// </summary>
+ public abstract bool IsVisible { get; }
+
+ /// <summary>
+ /// Creates a new <see cref="DisplayTextPoint"/> at this position that can be
+ /// moved independently from this one.
+ /// </summary>
+ new public DisplayTextPoint Clone()
+ {
+ return CloneDisplayTextPointInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="DisplayTextRange"/> that has this point and <paramref name="otherPoint"/>
+ /// as its start and end points.
+ /// </summary>
+ /// <returns>The <see cref="DisplayTextRange"/> that starts at this point and ends at <paramref name="otherPoint"/>.</returns>
+ /// <exception cref="InvalidOperationException"><paramref name="otherPoint"/> does not belong to the same buffer as this point, or
+ /// <paramref name="otherPoint"/> does not belong to the same view as this point.</exception>
+ public abstract DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="DisplayTextRange"/> that has this point and <paramref name="otherPosition"/>
+ /// as its start and end positions.
+ /// </summary>
+ /// <returns>The <see cref="DisplayTextRange"/> that starts at this point and ends at <paramref name="otherPosition"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="otherPosition"/> is in negative or past the end of this buffer.</exception>
+ public abstract DisplayTextRange GetDisplayTextRange(int otherPosition);
+
+ /// <summary>
+ /// Clones this <see cref="DisplayTextPoint"/>.
+ /// </summary>
+ /// <returns></returns>
+ protected sealed override TextPoint CloneInternal()
+ {
+ return CloneDisplayTextPointInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, clones this <see cref="DisplayTextPoint"/>.
+ /// </summary>
+ protected abstract DisplayTextPoint CloneDisplayTextPointInternal();
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/DisplayTextRange.cs b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs
new file mode 100644
index 0000000..9becda5
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs
@@ -0,0 +1,93 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.Text.Formatting;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Represents a range in the <see cref="TextBuffer"/> that behaves relative to the view in which it lives.
+ /// </summary>
+ public abstract class DisplayTextRange : TextRange, IEnumerable<DisplayTextPoint>
+ {
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextView"/> of this range.
+ /// </summary>
+ public abstract TextView TextView { get; }
+
+ /// <summary>
+ /// Creates a clone of this text range than can be moved independently of this one.
+ /// </summary>
+ public new DisplayTextRange Clone()
+ {
+ return CloneDisplayTextRangeInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the start point of this text range.
+ /// </summary>
+ public abstract DisplayTextPoint GetDisplayStartPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the end point of this text range.
+ /// </summary>
+ public abstract DisplayTextPoint GetDisplayEndPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the visibility state of this text range.
+ /// </summary>
+ public abstract VisibilityState Visibility { get; }
+
+ /// <summary>
+ /// Clones this text range.
+ /// </summary>
+ /// <returns>The cloned <see cref="TextRange"/>.</returns>
+ protected override TextRange CloneInternal()
+ {
+ return CloneDisplayTextRangeInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, clones the <see cref="DisplayTextRange"/>.
+ /// </summary>
+ protected abstract DisplayTextRange CloneDisplayTextRangeInternal();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the enumerator of type <see cref="DisplayTextPoint"/>.
+ /// </summary>
+ /// <returns></returns>
+ protected abstract IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal();
+
+ #region IEnumerable<DisplayTextPoint> Members
+
+ /// <summary>
+ /// Gets an enumerator of type <see cref="DisplayTextPoint"/>.
+ /// </summary>
+ /// <returns></returns>
+ public new IEnumerator<DisplayTextPoint> GetEnumerator()
+ {
+ return GetDisplayPointEnumeratorInternal();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Gets the enumerator.
+ /// </summary>
+ /// <returns></returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/HowToShow.cs b/src/Text/Def/Internal/TextUI/HowToShow.cs
new file mode 100644
index 0000000..4dec626
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/HowToShow.cs
@@ -0,0 +1,31 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Defines the ways to display a point or range.
+ /// </summary>
+ public enum HowToShow
+ {
+ /// <summary>
+ /// Show the point or start of the range as it is on screen, or scroll the
+ /// view the minimal amount in order to bring the point or range into view.
+ /// </summary>
+ AsIs,
+
+ /// <summary>
+ /// Show the point or start of the range centered on the screen.
+ /// </summary>
+ Centered,
+
+ /// <summary>
+ /// Show the point or start of the range on the first line of the view.
+ /// </summary>
+ OnFirstLineOfView,
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs b/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs
new file mode 100644
index 0000000..6acecbe
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs
@@ -0,0 +1,41 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Outlining
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+
+ /// <summary>
+ /// Extension of IOutliningManager to allow it to get accurate (if slow) results from the outlining taggers.
+ /// </summary>
+ /// <remarks>
+ /// <para>This interface only contains the minimal number of overloads of IOutliningManager methods to make restoring regions when opening a file
+ /// work. More overloads can be added as needed.</para>
+ /// </remarks>
+ public interface IAccurateOutliningManager : IOutliningManager
+ {
+ #region Collapsing and Expanding
+
+ /// <summary>
+ /// Collapses all regions that match the specified predicate.
+ /// </summary>
+ /// <param name="span">The regions that intersect this span.</param>
+ /// <param name="match">The predicate to match.</param>
+ /// <returns>The newly-collapsed regions.</returns>
+ /// <remarks>
+ /// The <paramref name="match"/> predicate may be passed regions that cannot actually be collapsed, due
+ /// to the region being partially obscured by another already collapsed region (either pre-existing or collapsed
+ /// in an earlier call to the predicate). The elements of the returned enumeration do accurately track
+ /// the regions that were collapsed, so they may differ from the elements for which the predicate returned <c>true</c>.
+ /// </remarks>
+ IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken cancel);
+
+ #endregion
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IAnnotationTag.cs b/src/Text/Def/Internal/TextUI/IAnnotationTag.cs
new file mode 100644
index 0000000..bd824df
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IAnnotationTag.cs
@@ -0,0 +1,56 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ public interface IAnnotationTag : ITag
+ {
+ /// <summary>
+ /// Can the user navigate to the location of this item (errors, find matches, collapsed regions).
+ /// </summary>
+ bool IsNavigable { get; }
+
+ /// <summary>
+ /// Some unique object where things of the same type (e.g. tracepoints) return the same object. Used to group similar things together.
+ /// </summary>
+ AnnotationKind ItemKindIdentifier { get; }
+
+ /// <summary>
+ /// What should be read out to indicate the existance of <paramref name="count"/> things of the same kind (e.g. "1 warning", or "2 errors").
+ /// </summary>
+ string ItemKindDisplayText(int count);
+ }
+
+ public abstract class AnnotationTag : IAnnotationTag
+ {
+ public virtual bool IsNavigable => false;
+
+ public abstract AnnotationKind ItemKindIdentifier { get; }
+
+ public abstract string ItemKindDisplayText(int count);
+ }
+
+ public enum AnnotationKind
+ {
+ Error = 1000,
+ Warning = 2000,
+ Message = 3000,
+ InstructionPointer = 3500,
+ Breakpoint = 4000,
+ Shortcut = 5000,
+ Tracepoint = 6000,
+ Bookmark = 7000,
+ CollapsedRegion = 8000,
+ ExpandedRegion = 9000,
+ Suggestion = 10000,
+ LineAddition = 11000,
+ LineDeletion = 12000,
+ WordChange = 13000
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs b/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs
new file mode 100644
index 0000000..cb633af
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs
@@ -0,0 +1,93 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.BraceCompletion
+{
+ /// <summary>
+ /// A per text view manager for brace completion.
+ /// </summary>
+ public interface IBraceCompletionManager
+ {
+ /// <summary>
+ /// Returns true if brace completion is enabled.
+ /// </summary>
+ bool Enabled { get; }
+
+ /// <summary>
+ /// Returns true if there are currently active sessions.
+ /// </summary>
+ bool HasActiveSessions { get; }
+
+ /// <summary>
+ /// Opening brace characters the brace completion manager is currently registered to handle.
+ /// </summary>
+ string OpeningBraces { get; }
+
+ /// <summary>
+ /// Closing brace characters the brace completion manager is currently registered to handle.
+ /// </summary>
+ string ClosingBraces { get; }
+
+ /// <summary>
+ /// Called by the editor when a character has been typed and before it is
+ /// inserted into the buffer.
+ /// </summary>
+ /// <param name="handledCommand">Set to true to prevent the closing brace character from being
+ /// inserted into the buffer.</param>
+ void PreTypeChar(char character, out bool handledCommand);
+
+ /// <summary>
+ /// Called by the editor after a character has been typed.
+ /// </summary>
+ void PostTypeChar(char character);
+
+ /// <summary>
+ /// Called by the editor when tab has been pressed and before it is inserted into the buffer.
+ /// </summary>
+ /// <param name="handledCommand">Set to true to prevent the tab from being inserted into the buffer.</param>
+ void PreTab(out bool handledCommand);
+
+ /// <summary>
+ /// Called by the editor after the tab has been inserted.
+ /// </summary>
+ void PostTab();
+
+ /// <summary>
+ /// Called by the editor before the character has been removed.
+ /// </summary>
+ /// <param name="handledCommand">Set to true to prevent the backspace action from completing.</param>
+ void PreBackspace(out bool handledCommand);
+
+ /// <summary>
+ /// Called by the editor after the character has been removed.
+ /// </summary>
+ void PostBackspace();
+
+ /// <summary>
+ /// Called by the editor when delete is pressed within the session.
+ /// </summary>
+ /// <param name="handledCommand">Set to true to prevent the deletion.</param>
+ void PreDelete(out bool handledCommand);
+
+ /// <summary>
+ /// Called by the editor after the delete action.
+ /// </summary>
+ void PostDelete();
+
+ /// <summary>
+ /// Called by the editor when return is pressed within the session.
+ /// </summary>
+ /// <param name="handledCommand">Set to true to prevent the new line insertion.</param>
+ void PreReturn(out bool handledCommand);
+
+ /// <summary>
+ /// Called by the editor after the new line has been inserted.
+ /// </summary>
+ void PostReturn();
+
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs b/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs
new file mode 100644
index 0000000..9d951a1
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs
@@ -0,0 +1,20 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Represents common buffer primitives and an extensible mechanism for replacing their values and adding new options.
+ /// </summary>
+ public interface IBufferPrimitives
+ {
+ /// <summary>
+ /// Gets the <see cref="TextBuffer"/> primitive used for text manipulation.
+ /// </summary>
+ TextBuffer Buffer { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs
new file mode 100644
index 0000000..20dec97
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs
@@ -0,0 +1,55 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Creates buffer primitives.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This factory is designed to be used by other primitives. Consumers of the primitives
+ /// should use the <see cref="IEditorPrimitivesFactoryService"/> to get a reference to a <see cref="TextBuffer"/>
+ /// and use that to create the primitives.
+ /// </para>
+ /// </remarks>
+ /// <remarks>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// IBufferPrimitivesFactoryService factory = null;
+ /// </remarks>
+ public interface IBufferPrimitivesFactoryService
+ {
+ /// <summary>
+ /// Creates a <see cref="TextBuffer"/> primitive.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="ITextBuffer"/> on which to base this primitive.</param>
+ /// <returns>The <see cref="TextBuffer"/> primitive for the given <see cref="ITextBuffer"/>.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method always returns the same object if the same <see cref="ITextBuffer"/> is passed in.
+ /// </para>
+ /// </remarks>
+ TextBuffer CreateTextBuffer(ITextBuffer textBuffer);
+
+ /// <summary>
+ /// Creates a new <see cref="TextPoint"/> primitive.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="TextBuffer"/> to which this <see cref="TextPoint"/> belongs.</param>
+ /// <param name="position">The position of this <see cref="TextPoint"/>.</param>
+ /// <returns>A new <see cref="TextPoint"/> primitive at the given <paramref name="position"/>.</returns>
+ TextPoint CreateTextPoint(TextBuffer textBuffer, int position);
+
+ /// <summary>
+ /// Creates a new <see cref="TextRange"/> primitive.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="TextBuffer"/> to which this <see cref="TextPoint"/> belongs.</param>
+ /// <param name="startPoint">The <see cref="TextPoint"/> of the start.</param>
+ /// <param name="endPoint">The <see cref="TextPoint"/> of the end.</param>
+ /// <returns>A new <see cref="TextRange"/> primitive at the given <paramref name="startPoint"/> and <paramref name="endPoint"/>.</returns>
+ TextRange CreateTextRange(TextBuffer textBuffer, TextPoint startPoint, TextPoint endPoint);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs
new file mode 100644
index 0000000..d842b50
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs
@@ -0,0 +1,33 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// A service that provides primitives for a given <see cref="ITextView"/> or <see cref="ITextBuffer"/>.
+ /// </summary>
+ /// <remarks>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// IEditorPrimitivesFactoryService factory = null;
+ /// </remarks>
+ public interface IEditorPrimitivesFactoryService
+ {
+ /// <summary>
+ /// Gets the <see cref="IViewPrimitives"/> for the <see cref="ITextView"/>.
+ /// </summary>
+ /// <param name="textView">The <see cref="ITextView"/> for which to get the <see cref="IViewPrimitives"/>.</param>
+ /// <returns>The <see cref="IViewPrimitives"/> for the given <see cref="ITextView"/>.</returns>
+ IViewPrimitives GetViewPrimitives(ITextView textView);
+
+ /// <summary>
+ /// Gets the <see cref="IBufferPrimitives"/> for the <see cref="ITextBuffer"/>.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="ITextBuffer"/> for which to fetch <see cref="IBufferPrimitives"/>.</param>
+ /// <returns>The <see cref="IBufferPrimitives"/> for the given <see cref="ITextBuffer"/>.</returns>
+ IBufferPrimitives GetBufferPrimitives(ITextBuffer textBuffer);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IMapEditToData.cs b/src/Text/Def/Internal/TextUI/IMapEditToData.cs
new file mode 100644
index 0000000..989edc5
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IMapEditToData.cs
@@ -0,0 +1,16 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+
+ public interface IMapEditToData
+ {
+ int MapEditToData(int editPoint);
+ int MapDataToEdit(int dataPoint);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IObscuringTip.cs b/src/Text/Def/Internal/TextUI/IObscuringTip.cs
new file mode 100644
index 0000000..d0f2c46
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IObscuringTip.cs
@@ -0,0 +1,36 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ public interface IObscuringTip
+ {
+ /// <summary>
+ /// Dismiss the tip. Return true if the tip had been visible.
+ /// </summary>
+ bool Dismiss();
+
+ /// <summary>
+ /// Get the current opacity of the tip (should be 100% unless explicitly set otherwise).
+ /// </summary>
+ double Opacity { get; }
+
+ /// <summary>
+ /// Set the opacity of the tip (generally to either 100% or 10% while the control key is held down.
+ /// </summary>
+ void SetOpacity(double opacity);
+ }
+
+ public abstract class Tip : IObscuringTip
+ {
+ public abstract bool Dismiss();
+
+ public virtual double Opacity => 1.0;
+
+ public virtual void SetOpacity(double opacity) { }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs b/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs
new file mode 100644
index 0000000..43e8de5
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs
@@ -0,0 +1,19 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Class used to manage tips displayed in a view.
+ /// </summary>
+ public interface IObscuringTipManager
+ {
+ void PushTip(ITextView view, IObscuringTip tip);
+
+ void RemoveTip(ITextView view, IObscuringTip tip);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IStructureTipManager.cs b/src/Text/Def/Internal/TextUI/IStructureTipManager.cs
new file mode 100644
index 0000000..eb926d8
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IStructureTipManager.cs
@@ -0,0 +1,25 @@
+namespace Microsoft.VisualStudio.Text.Structure
+{
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// Facilitates invocation of Structure Guide Lines tooltip.
+ /// </summary>
+ public interface IStructureTipManager
+ {
+ /// <summary>
+ /// Gets whether or not Structure Tips are available in the current view.
+ /// </summary>
+ /// <param name="textView">The current view.</param>
+ /// <returns>Returns true if structure tips are available.</returns>
+ bool CanTriggerStructureTip(ITextView textView);
+
+ /// <summary>
+ /// Displays the structure guide lines tooltip containing the context at the
+ /// specified trigger point.
+ /// </summary>
+ /// <param name="textView">The textview to display the tip for.</param>
+ /// <param name="point">The point to display context for.</param>
+ void TriggerStructureTip(ITextView textView, SnapshotPoint point);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/ITextView2.cs b/src/Text/Def/Internal/TextUI/ITextView2.cs
new file mode 100644
index 0000000..38ff665
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/ITextView2.cs
@@ -0,0 +1,46 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System;
+
+ /// <summary>
+ /// An extension of the ITextView that exposes some internal hooks.
+ /// </summary>
+ public interface ITextView2 : ITextView
+ {
+ /// <summary>
+ /// The MaxTextRightCoordinate of the view based only on the text contained in the view.
+ /// </summary>
+ double RawMaxTextRightCoordinate
+ {
+ get;
+ }
+
+ /// <summary>
+ /// The minimum value for the view's MaxTextRightCoordinate.
+ /// </summary>
+ /// <remarks>
+ /// If setting this value changes the view's MaxTextRightCoordinate, the view will raise a layout changed event.
+ /// </remarks>
+ double MinMaxTextRightCoordinate
+ {
+ get;
+ set;
+ }
+
+ /// <summary>
+ /// Raised whenever the view's MaxTextRightCoordinate is changed.
+ /// </summary>
+ /// <remarks>
+ /// This event will only be rasied if the MaxTextRightCoordinate is changed by changing the MinMaxTextRightCoordinate property
+ /// (it will not be raised as a side-effect of a layout even if the layout does change the MaxTextRightCoordinate).
+ /// </remarks>
+ event EventHandler MaxTextRightCoordinateChanged;
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs b/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs
new file mode 100644
index 0000000..ad640eb
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs
@@ -0,0 +1,20 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ public interface IThumbnailSupport
+ {
+ /// <summary>
+ /// Controls whether or note the view's visuals are removed when the view is hidden.
+ /// </summary>
+ /// <remarks>
+ /// Defaults to true and is should to false when generating thumbnails of a hidden view (then restored afterwards).
+ /// </remarks>
+ bool RemoveVisualsWhenHidden { get; set; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitives.cs b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs
new file mode 100644
index 0000000..4130715
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs
@@ -0,0 +1,57 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+
+ /// <summary>
+ /// Represents common view primitives and an extensible mechanism for replacing their values and adding new options.
+ /// </summary>
+ public interface IViewPrimitives : IBufferPrimitives
+ {
+ /// <summary>
+ /// Gets the <see cref="View"/> primitive used for scrolling the editor window.
+ /// </summary>
+ TextView View { get; }
+
+ /// <summary>
+ /// Gets the <see cref="Selection"/> primitive used for selection manipulation.
+ /// </summary>
+ Selection Selection { get; }
+
+ /// <summary>
+ /// Gets the <see cref="Caret"/> primitive used for caret movement.
+ /// </summary>
+ Caret Caret { get; }
+ }
+
+ /// <summary>
+ /// Represents the common editor primitives produced by this subsystem.
+ /// </summary>
+ public static class EditorPrimitiveIds
+ {
+ /// <summary>
+ /// The ID for the view.
+ /// </summary>
+ public const string ViewPrimitiveId = "Editor.View";
+
+ /// <summary>
+ /// The ID for the selection.
+ /// </summary>
+ public const string SelectionPrimitiveId = "Editor.Selection";
+
+ /// <summary>
+ /// The ID for the caret.
+ /// </summary>
+ public const string CaretPrimitiveId = "Editor.Caret";
+
+ /// <summary>
+ /// The ID for the buffer.
+ /// </summary>
+ public const string BufferPrimitiveId = "Editor.TextBuffer";
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs
new file mode 100644
index 0000000..2408671
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs
@@ -0,0 +1,78 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Creates view primitives.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This factory is designed to be used by other primitives. Consumers of the primitives
+ /// should use the <see cref="IEditorPrimitivesFactoryService"/> to get a reference to a
+ /// <see cref="TextView"/> and use that to create the primitives.
+ /// </para>
+ /// </remarks>
+ /// <remarks>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// IViewPrimitivesFactoryService factory = null;
+ /// </remarks>
+ public interface IViewPrimitivesFactoryService
+ {
+ /// <summary>
+ /// Creates a <see cref="TextView"/> primitive.
+ /// </summary>
+ /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param>
+ /// <returns>The <see cref="TextView"/> primitive for the given <see cref="ITextView"/>.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method always returns the same object if the same <see cref="ITextView"/> is passed in.
+ /// </para>
+ /// </remarks>
+ TextView CreateTextView(ITextView textView);
+
+ /// <summary>
+ /// Creates a new <see cref="DisplayTextPoint"/> primitive.
+ /// </summary>
+ /// <param name="textView">The <see cref="TextView"/> to which this <see cref="DisplayTextPoint"/> belongs.</param>
+ /// <param name="position">The position of this <see cref="DisplayTextPoint"/>.</param>
+ /// <returns>A new <see cref="DisplayTextPoint"/> primitive at the given <paramref name="position"/>.</returns>
+ DisplayTextPoint CreateDisplayTextPoint(TextView textView, int position);
+
+ /// <summary>
+ /// Creates a new <see cref="DisplayTextRange"/> primitive.
+ /// </summary>
+ /// <param name="textView">The <see cref="TextView"/> to which this <see cref="DisplayTextRange"/> belongs.</param>
+ /// <param name="textRange">The <see cref="TextRange"/> in the <see cref="TextBuffer"/>.</param>
+ /// <returns>A new <see cref="DisplayTextRange"/> primitive at the given <paramref name="textView"/> and <paramref name="textRange"/>.</returns>
+ DisplayTextRange CreateDisplayTextRange(TextView textView, TextRange textRange);
+
+ /// <summary>
+ /// Creates a <see cref="Selection"/> primitive.
+ /// </summary>
+ /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param>
+ /// <returns>The <see cref="Selection"/> primitive for the given <see cref="ITextView"/>.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method always returns the same object if the same <see cref="ITextView"/> is passed in.
+ /// </para>
+ /// </remarks>
+ Selection CreateSelection(TextView textView);
+
+ /// <summary>
+ /// Creates a <see cref="Caret"/> primitive.
+ /// </summary>
+ /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param>
+ /// <returns>The <see cref="Caret"/> primitive for the given <see cref="ITextView"/>.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method always returns the same object if the same <see cref="ITextView"/> is passed in.
+ /// </para>
+ /// </remarks>
+ Caret CreateCaret(TextView textView);
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs b/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs
new file mode 100644
index 0000000..3cff2ac
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs
@@ -0,0 +1,17 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.OverviewMargin
+{
+ public static class OverviewFormatDefinitions
+ {
+ public const string ElisionColorName = "OverviewMarginCollapsedRegion";
+ public const string OffScreenColorName = "OverviewMarginBackground";
+ public const string VisibleColorName = "OverviewMarginVisible";
+ public const string CaretColorName = "OverviewMarginCaret";
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/Selection.cs b/src/Text/Def/Internal/TextUI/Selection.cs
new file mode 100644
index 0000000..59d27eb
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/Selection.cs
@@ -0,0 +1,76 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System;
+
+ /// <summary>
+ /// Represents the selection on the screen.
+ /// </summary>
+ public abstract class Selection : DisplayTextRange
+ {
+ /// <summary>
+ /// When implemented in a derived class, selects the given text range.
+ /// </summary>
+ /// <param name="textRange">The range to select.</param>
+ public abstract void SelectRange(TextRange textRange);
+
+ /// <summary>
+ /// When implemented in a derived class, selects the given text range, reversing the selection if needed. Ensures
+ /// that the end point of the selection is visible on screen.
+ /// </summary>
+ /// <param name="selectionStart">The start point for the selection.</param>
+ /// <param name="selectionEnd">The end point for the selection.</param>
+ /// <remarks>If <paramref name="selectionStart"/> is positioned after <paramref name="selectionEnd"/>, then the
+ /// selection is reversed.</remarks>
+ public abstract void SelectRange(TextPoint selectionStart, TextPoint selectionEnd);
+
+ /// <summary>
+ /// When implemented in a derived class, selects all the text in the document. Ensures that the end point
+ /// of the selection is visible on screen.
+ /// </summary>
+ public abstract void SelectAll();
+
+ /// <summary>
+ /// When implemented in a derived class, extends the selection from its current start point to the new end point. Ensures
+ /// that the end point of the selection is visible on screen.
+ /// </summary>
+ /// <param name="newEnd">
+ /// The text point to which to extend the selection.
+ /// </param>
+ /// <remarks>
+ /// <paramref name="newEnd"/> may become the new start point, if <paramref name="newEnd"/> is before the current start point.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException"><paramref name="newEnd"/> belongs to a different buffer.</exception>
+ public abstract void ExtendSelection(TextPoint newEnd);
+
+ /// <summary>
+ /// When implemented in a derived class, resets any selection in the text.
+ /// </summary>
+ public abstract void Clear();
+
+ /// <summary>
+ /// When implemented in a derived class, provides advanced selection manipulation functionality.
+ /// </summary>
+ /// <returns>The <see cref="ITextSelection"/>.</returns>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract ITextSelection AdvancedSelection
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, determines whether the end point represents the start of the selection.
+ /// </summary>
+ public abstract bool IsReversed
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/TextBuffer.cs b/src/Text/Def/Internal/TextUI/TextBuffer.cs
new file mode 100644
index 0000000..8343b5d
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/TextBuffer.cs
@@ -0,0 +1,103 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// Provides methods for text insertion, deletion, and modification.
+ /// </summary>
+ public abstract class TextBuffer
+ {
+ /// <summary>
+ /// When implemented in a derived class, gets a text point from a integer position.
+ /// </summary>
+ /// <param name="position">The position at which to get the text point.</param>
+ /// <returns>The <see cref="TextPoint"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of the buffer.</exception>
+ public abstract TextPoint GetTextPoint(int position);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a text point from a line and column.
+ /// </summary>
+ /// <param name="line">The line on which to get this text point.</param>
+ /// <param name="column">The line-relative position at which to get the text point.</param>
+ /// <returns>The <see cref="TextPoint"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer, or
+ /// <paramref name="column"/> is negative or past the end of the line.</exception>
+ /// <remarks>
+ /// The line and column are zero-based.
+ /// </remarks>
+ public abstract TextPoint GetTextPoint(int line, int column);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a <see cref="TextRange"/> representing a line in the <see cref="TextBuffer"/>.
+ /// </summary>
+ /// <param name="line">The line.</param>
+ /// <returns>A <see cref="TextRange"/> representing a line in the <see cref="TextBuffer"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer.</exception>
+ /// <remarks>
+ /// <para>
+ /// The <see cref="TextRange"/> returned does not include the line break characters for the line.
+ /// </para>
+ /// </remarks>
+ public abstract TextRange GetLine(int line);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a text range from two text points.
+ /// </summary>
+ /// <param name="startPoint">The start point of the range.</param>
+ /// <param name="endPoint">The end point of the range.</param>
+ /// <returns>The text range that starts and ends at the two points.</returns>
+ /// <remarks>The start point of the text range may become the end point if the start point is after the end point.</remarks>
+ /// <exception cref="InvalidOperationException"><paramref name="startPoint"/> or <paramref name="endPoint"/> do not belong to this buffer.</exception>
+ public abstract TextRange GetTextRange(TextPoint startPoint, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a text range from two integer positions.
+ /// </summary>
+ /// <param name="startPosition">The start position of the range.</param>
+ /// <param name="endPosition">The end position of the range.</param>
+ /// <returns>The text range that starts and ends at the two positions.</returns>
+ /// <remarks>The start position of the text range may become the end point if the start position is after the end position.</remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="startPosition"/> is negative or past the end of the buffer.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="endPosition"/> is negative or past the end of the buffer.</exception>
+ public abstract TextRange GetTextRange(int startPosition, int endPosition);
+
+ /// <summary>
+ /// When implemented in a derived class, provides advanced text manipulation functionality.
+ /// </summary>
+ /// <returns>The <see cref="ITextBuffer"/>.</returns>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract ITextBuffer AdvancedTextBuffer
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the start point of the buffer (always zero).
+ /// </summary>
+ /// <returns>The starting <see cref="TextPoint"/>.</returns>
+ public abstract TextPoint GetStartPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the end point of the buffer (always the last position in the buffer.
+ /// </summary>
+ /// <returns>The end <see cref="TextPoint"/>.</returns>
+ public abstract TextPoint GetEndPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> objectss representing lines in the buffer.
+ /// </summary>
+ public abstract IEnumerable<TextRange> Lines
+ {
+ get;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/TextPoint.cs b/src/Text/Def/Internal/TextUI/TextPoint.cs
new file mode 100644
index 0000000..6170a8d
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/TextPoint.cs
@@ -0,0 +1,357 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.VisualStudio.Text.Operations;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Represents a point in the <see cref="TextBuffer"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// While this point is immutable, its position may change in response
+ /// to edits in the text.
+ /// </para>
+ /// </remarks>
+ public abstract class TextPoint
+ {
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextBuffer"/> of this point.
+ /// </summary>
+ public abstract TextBuffer TextBuffer { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the integer representation of the current position of this text point
+ /// in relation to the start of the buffer.
+ /// </summary>
+ public abstract int CurrentPosition { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the integer representation of the current position of this text point
+ /// in relation to the start of the line this point is on.
+ /// </summary>
+ public abstract int Column { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, deletes the character after this text point.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool DeleteNext();
+
+ /// <summary>
+ /// When implemented in a derived class, deletes the character before this text point.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool DeletePrevious();
+
+ /// <summary>
+ /// When implemented in a derived class, gets a text point for the first non-whitespace
+ /// character on the current line.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// If a line is all white space, this method returns a <see cref="TextPoint"/> at the end of the line, but before the
+ /// line break.
+ /// </para>
+ /// </remarks>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract TextPoint GetFirstNonWhiteSpaceCharacterOnLine();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> of the current word. The current word may be white space only.
+ /// </summary>
+ /// <returns>The <see cref="TextRange"/> of the current word.</returns>
+ public abstract TextRange GetCurrentWord();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> of the next word that is not white space.
+ /// </summary>
+ /// <returns>The <see cref="TextRange"/> of the next word.</returns>
+ public abstract TextRange GetNextWord();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> of the previous word that is not white space.
+ /// </summary>
+ /// <returns>The <see cref="TextRange"/> of the previous word.</returns>
+ public abstract TextRange GetPreviousWord();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> that has this point and <paramref name="otherPoint"/>
+ /// as its start and end points.
+ /// </summary>
+ /// <returns>The <see cref="TextRange"/> that starts at this point and ends at <paramref name="otherPoint"/>.</returns>
+ /// <exception cref="InvalidOperationException"><paramref name="otherPoint"/> does not belong to the same buffer as this point.</exception>
+ public abstract TextRange GetTextRange(TextPoint otherPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> that has this point and <paramref name="otherPosition"/>
+ /// as its start and end positions.
+ /// </summary>
+ /// <returns>The <see cref="TextRange"/> that starts at this point and ends at <paramref name="otherPosition"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="otherPosition"/> is negative or past the end of this buffer.</exception>
+ public abstract TextRange GetTextRange(int otherPosition);
+
+ /// <summary>
+ /// When implemented in a derived class, inserts a new line character at this text point.
+ /// </summary>
+ /// <returns>
+ /// Whether the edit succeeded.
+ /// </returns>
+ public abstract bool InsertNewLine();
+
+ /// <summary>
+ /// When implemented in a derived class, inserts a logical tab at this text point.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool InsertIndent();
+
+ /// <summary>
+ /// When implemented in a derived class, inserts <paramref name="text"/> at this text point.
+ /// </summary>
+ /// <param name="text">The text to insert.</param>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool InsertText(string text);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the line this text point is on.
+ /// </summary>
+ public abstract int LineNumber { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the position of the start of the line this text point is on.
+ /// </summary>
+ public abstract int StartOfLine { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the position of the end of the line this text point is on.
+ /// </summary>
+ public abstract int EndOfLine { get; }
+
+ /// <summary>
+ /// When implemented in a derived class, removes a logical tab before this text point.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool RemovePreviousIndent();
+
+ /// <summary>
+ /// When implemented in a derived class, transposes the two characters on either side of this text point.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool TransposeCharacter();
+
+ /// <summary>
+ /// When implemented in a derived class, transposes the line this point is one with the next line. If this point is on the last
+ /// line of the file, the line is transposed with the previous one.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool TransposeLine();
+
+ /// <summary>
+ /// When implemented in a derived class, transposes the line this point is one with the given line number.
+ /// </summary>
+ /// <param name="lineNumber">The line number with which to transpose the line this point is on.</param>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool TransposeLine(int lineNumber);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the underlying <see cref="SnapshotPoint"/> of this text point.
+ /// </summary>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract SnapshotPoint AdvancedTextPoint
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the next character after this text point.
+ /// </summary>
+ public abstract string GetNextCharacter();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the previous character before this text point.
+ /// </summary>
+ /// <returns>The previous character.</returns>
+ public abstract string GetPreviousCharacter();
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <param name="endPoint">The text point at which to stop searching.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns>
+ public abstract TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="endPoint">The text point at which to stop searching.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns>
+ public abstract TextRange Find(string pattern, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> starting from this text point.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns>
+ public abstract TextRange Find(string pattern, FindOptions findOptions);
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> starting from this text point.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns>
+ public abstract TextRange Find(string pattern);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="endPoint">The text point at which to stop searching.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="endPoint">The text point at which to stop searching.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting from this text point.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting from this text point.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions);
+
+ /// <summary>
+ /// When implemented in a derived class, moves this text point to a specific location.
+ /// </summary>
+ /// <param name="position">The new position of this text point.</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of this buffer.</exception>
+ public abstract void MoveTo(int position);
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the next character in the buffer.
+ /// </summary>
+ public abstract void MoveToNextCharacter();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the previous character in the buffer.
+ /// </summary>
+ ///
+ public abstract void MoveToPreviousCharacter();
+
+ /// <summary>
+ /// Creates a new <see cref="TextPoint"/> at this position that can be
+ /// moved independently from this one.
+ /// </summary>
+ /// <returns>The cloned <see cref="TextPoint"/>.</returns>
+ public TextPoint Clone()
+ {
+ return CloneInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, clones the <see cref="TextPoint"/>.
+ /// </summary>
+ /// <returns>The cloned <see cref="TextPoint"/>.</returns>
+ protected abstract TextPoint CloneInternal();
+
+ /// <summary>
+ /// When implemented in a derived class, moves the text point at the start of the specified line and ensures it is visible.
+ /// </summary>
+ /// <param name="lineNumber">
+ /// The line number on which to position the text point.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero
+ /// or greater than the line number of the last line in the text buffer.</exception>
+ public abstract void MoveToLine(int lineNumber);
+
+ /// <summary>
+ /// When implemented in a derived class, movea this point to the end of the line that it is currently on.
+ /// </summary>
+ public abstract void MoveToEndOfLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the start of the line that it is currently on.
+ /// </summary>
+ public abstract void MoveToStartOfLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the end of the document.
+ /// </summary>
+ public abstract void MoveToEndOfDocument();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the start of the document.
+ /// </summary>
+ public abstract void MoveToStartOfDocument();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the beginning of the next line.
+ /// </summary>
+ /// <remarks>
+ /// This point moves to the end of the line if the point is on the last
+ /// line.
+ /// </remarks>
+ public abstract void MoveToBeginningOfNextLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the beginning of the previous line.
+ /// </summary>
+ /// <remarks>
+ /// This point moves to the start of the line if the point is on the first
+ /// line.
+ /// </remarks>
+ public abstract void MoveToBeginningOfPreviousLine();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the end of the current word, or to the beginning of the
+ /// next word if the point is already at the end of the current word.
+ /// </summary>
+ public abstract void MoveToNextWord();
+
+ /// <summary>
+ /// When implemented in a derived class, moves this point to the start of the current word, or the end of the
+ /// previous word if the point is already at the start of the current word.
+ /// </summary>
+ public abstract void MoveToPreviousWord();
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/TextRange.cs b/src/Text/Def/Internal/TextUI/TextRange.cs
new file mode 100644
index 0000000..3b114ad
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/TextRange.cs
@@ -0,0 +1,257 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Collections;
+using System.Collections.ObjectModel;
+using Microsoft.VisualStudio.Text.Operations;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Represents a range of text in the buffer.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// While this range is immutable, edits to the text will cause
+ /// it to adjust its location in response to the edits.
+ /// </para>
+ /// </remarks>
+ public abstract class TextRange : IEnumerable<TextPoint>
+ {
+ /// <summary>
+ /// When implemented in a derived class, gets the start point of this text range.
+ /// </summary>
+ /// <returns>The starting <see cref="TextPoint"/>.</returns>
+ public abstract TextPoint GetStartPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the end point of this text range.
+ /// </summary>
+ /// <returns>The end <see cref="TextPoint"/>.</returns>
+ public abstract TextPoint GetEndPoint();
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextBuffer"/> of this text range.
+ /// </summary>
+ /// <returns>The <see cref="TextBuffer"/> of this text range.</returns>
+ public abstract TextBuffer TextBuffer
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the underlying <see cref="SnapshotSpan"/> of this <see cref="TextRange"/>.
+ /// The <see cref="SnapshotSpan"/> should be used only for advanced functionality.
+ /// </summary>
+ /// <returns>The <see cref="SnapshotSpan"/>.</returns>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public abstract SnapshotSpan AdvancedTextRange
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, makes the text in this range uppercase.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ /// <remarks><para>If the range is empty, will apply to the character next to the range only.</para></remarks>
+ public abstract bool MakeUppercase();
+
+ /// <summary>
+ /// When implemented in a derived class, makes the text in the this range lowercase.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ /// <remarks><para>If the range is empty, this method applies to the character next to the range only.</para></remarks>
+ public abstract bool MakeLowercase();
+
+ /// <summary>
+ /// When implemented in a derived class, makes the first character in every word in this range uppercase, and makes the rest of the characters lowercase.
+ /// </summary>
+ /// <remarks>
+ /// If the range is empty, this method applies to the character next to the range only.
+ /// If the range starts in the middle of a word, only the part in the range will be made lowercase.
+ /// </remarks>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool Capitalize();
+
+ /// <summary>
+ /// When implemented in a derived class, switches the case of every character in this range.
+ /// </summary>
+ /// <remarks><para>If the range is empty, this method applies to the character next to the range only.</para></remarks>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool ToggleCase();
+
+ /// <summary>
+ /// When implemented in a derived class, deletes all the text in this range.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool Delete();
+
+ /// <summary>
+ /// When implemented in a derived class, indents all the lines in this range.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool Indent();
+
+ /// <summary>
+ /// When implemented in a derived class, unindents all the lines in this range.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool Unindent();
+
+ /// <summary>
+ /// When implemented in a derived class, determines whether the <see cref="TextRange"/> is zero-length.
+ /// </summary>
+ /// <returns><c>true</c> if the <see cref="TextRange"/> is zero length, <c>false</c> otherwise.</returns>
+ public abstract bool IsEmpty
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> in this text range.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at the start point of this range.</returns>
+ public abstract TextRange Find(string pattern);
+
+ /// <summary>
+ /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> in this text range.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at the start point of this range.</returns>
+ public abstract TextRange Find(string pattern, FindOptions findOptions);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting in this text range.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern);
+
+ /// <summary>
+ /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting in this text range.
+ /// </summary>
+ /// <param name="pattern">The pattern to find.</param>
+ /// <param name="findOptions">The options to use while searching.</param>
+ /// <returns>A list of matches in the order they were found.</returns>
+ public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions);
+
+ /// <summary>
+ /// When implemented in a derived class, replaces the text in this range with <paramref name="newText"/>.
+ /// </summary>
+ /// <param name="newText">The new text.</param>
+ /// <remarks>
+ /// This <see cref="TextRange"/> spans the new text after it has been replaced.
+ /// </remarks>
+ /// <returns>
+ /// <c>true</c> if the edit succeeded, otherwise <c>false</c>.
+ /// </returns>
+ public abstract bool ReplaceText(string newText);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the text in this range.
+ /// </summary>
+ /// <returns>The text in the range.</returns>
+ public abstract string GetText();
+
+ /// <summary>
+ /// When implemented in a derived class, creates a clone of this text range than can be moved independently of this one.
+ /// </summary>
+ /// <returns>The cloned <see cref="TextRange"/>.</returns>
+ public TextRange Clone()
+ {
+ return CloneInternal();
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, clones the text range.
+ /// </summary>
+ /// <returns>The cloned <see cref="TextRange"/>.</returns>
+ protected abstract TextRange CloneInternal();
+
+ /// <summary>
+ /// When implemented in a derived class, sets the start point of this text range.
+ /// </summary>
+ /// <param name="startPoint">The new start point.</param>
+ /// <remarks>
+ /// If <paramref name="startPoint"/> occurs after
+ /// the current end point in the buffer, <paramref name="startPoint"/> becomes the end point, and the current end point becomes the start point.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException"><paramref name="startPoint"/> belongs to another buffer.</exception>
+ public abstract void SetStart(TextPoint startPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, sets the end point of this text range.
+ /// </summary>
+ /// <param name="endPoint">The new end point.</param>
+ /// <remarks>
+ /// <para>
+ /// If <paramref name="endPoint"/> is before
+ /// the current start point in the buffer, <paramref name="endPoint"/> becomes the start point, and the current start point becomes the end point.
+ /// </para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException"><paramref name="endPoint"/> belongs to another buffer.</exception>
+ public abstract void SetEnd(TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, moves this text range to the range of <paramref name="newRange"/>.
+ /// </summary>
+ /// <param name="newRange">The new range.</param>
+ public abstract void MoveTo(TextRange newRange);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the enumerator of type <see cref="TextPoint"/>.
+ /// </summary>
+ /// <returns>the enumerator of type <see cref="TextPoint"/>.</returns>
+ protected abstract IEnumerator<TextPoint> GetEnumeratorInternal();
+
+ #region IEnumerable<TextPoint> Members
+
+ /// <summary>
+ /// Gets the enumerator.
+ /// </summary>
+ /// <returns>An enumerator of type <see cref="TextPoint"/>.</returns>
+ public IEnumerator<TextPoint> GetEnumerator()
+ {
+ return GetEnumeratorInternal();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Gets the enumerator.
+ /// </summary>
+ /// <returns>The enumerator.</returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/TextView.cs b/src/Text/Def/Internal/TextUI/TextView.cs
new file mode 100644
index 0000000..e1a3fa6
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/TextView.cs
@@ -0,0 +1,165 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System;
+ using Microsoft.VisualStudio.Text.Formatting;
+
+ /// <summary>
+ /// Provides methods for scrolling the editor window up and down.
+ /// </summary>
+ public abstract class TextView
+ {
+ /// <summary>
+ /// When implemented in a derived class, moves the current line to the top of the view without moving the caret.
+ /// </summary>
+ /// <param name="lineNumber">The number of lines to move.</param>
+ public abstract void MoveLineToTop(int lineNumber);
+
+ /// <summary>
+ /// When implemented in a derived class, moves the current line to the bottom of the view without moving the caret.
+ /// </summary>
+ /// <param name="lineNumber">The number of lines to move.</param>
+ public abstract void MoveLineToBottom(int lineNumber);
+
+ /// <summary>
+ /// When implemented in a derived class, scrolls the view up by one line.
+ /// </summary>
+ /// <param name="lines">The number of lines to scroll.</param>
+ public abstract void ScrollUp(int lines);
+
+ /// <summary>
+ /// When implemented in a derived class, scrolls the view down by one line.
+ /// </summary>
+ /// <param name="lines">The number of lines to scroll.</param>
+ public abstract void ScrollDown(int lines);
+
+ /// <summary>
+ /// When implemented in a derived class, scrolls the view down by one page and does not move the caret.
+ /// </summary>
+ public abstract void ScrollPageDown();
+
+ /// <summary>
+ /// When implemented in a derived class, scrolls the view up by one page and does not move the caret.
+ /// </summary>
+ public abstract void ScrollPageUp();
+
+ /// <summary>
+ /// When implemented in a derived class, shows the <paramref name="point"/> in the view.
+ /// </summary>
+ /// <param name="point">The point to display.</param>
+ /// <param name="howToShow">How the point should be displayed on the screen.</param>
+ /// <returns><c>true</c> if the point was actually displayed, otherwise <c>false</c>.</returns>
+ public abstract bool Show(DisplayTextPoint point, HowToShow howToShow);
+
+ /// <summary>
+ /// When implemented in a derived class, shows the <paramref name="textRange"/> in the view.
+ /// </summary>
+ /// <param name="textRange">The <see cref="TextRange"/> to display.</param>
+ /// <param name="howToShow">How the point should be displayed on the screen.</param>
+ /// <returns>The <see cref="VisibilityState"/> that describes how the range was actually displayed.</returns>
+ public abstract VisibilityState Show(DisplayTextRange textRange, HowToShow howToShow);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text point from a integer position.
+ /// </summary>
+ /// <param name="position">The position at which to get the text point.</param>
+ /// <returns>The <see cref="DisplayTextPoint"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of the buffer.</exception>
+ public abstract DisplayTextPoint GetTextPoint(int position);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text point from a buffer text point position.
+ /// </summary>
+ /// <param name="textPoint">The buffer text point to translate into a display text point.</param>
+ /// <returns>The <see cref="DisplayTextPoint"/>.</returns>
+ /// <exception cref="ArgumentException"><paramref name="textPoint"/> does not belong to the same buffer as the view.</exception>
+ public abstract DisplayTextPoint GetTextPoint(TextPoint textPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text point from a line and column.
+ /// </summary>
+ /// <param name="line">The line on which to get this text point.</param>
+ /// <param name="column">The line-relative position at which to get the text point.</param>
+ /// <returns>The <see cref="DisplayTextPoint"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer, or
+ /// <paramref name="column"/> is negative or past the end of the line.</exception>
+ public abstract DisplayTextPoint GetTextPoint(int line, int column);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text range from two display text points.
+ /// </summary>
+ /// <param name="startPoint">The start point of the range.</param>
+ /// <param name="endPoint">The end point of the range.</param>
+ /// <returns>The text range that starts and ends at the two points.</returns>
+ /// <remarks>The start point of the text range may become the end point if the start point occurs after the end point.</remarks>
+ /// <exception cref="ArgumentException"><paramref name="startPoint"/> or <paramref name="endPoint"/> do not belong to this buffer.</exception>
+ public abstract DisplayTextRange GetTextRange(TextPoint startPoint, TextPoint endPoint);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text range from a text range on the buffer.
+ /// </summary>
+ /// <param name="textRange">The text range on the buffer.</param>
+ /// <returns>The <see cref="DisplayTextPoint"/>.</returns>
+ /// <remarks>The start point of the text range may become the end point if the start point occurs after the end point.</remarks>
+ /// <exception cref="ArgumentException"><paramref name="textRange"/> does not belong to the same buffer as the view.</exception>
+ public abstract DisplayTextRange GetTextRange(TextRange textRange);
+
+ /// <summary>
+ /// When implemented in a derived class, gets a display text range from two integer positions.
+ /// </summary>
+ /// <param name="startPosition">The start position of the range.</param>
+ /// <param name="endPosition">The end position of the range.</param>
+ /// <returns>The text range that starts and ends at the two positions.</returns>
+ /// <remarks>The start position of the text range may become the end point if the start position occurs after the end position.</remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="startPosition"/> is negative or past the end of the buffer.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="endPosition"/> is negative or past the end of the buffer.</exception>
+ public abstract DisplayTextRange GetTextRange(int startPosition, int endPosition);
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextRange"/> of text currently visible on screen.
+ /// </summary>
+ public abstract DisplayTextRange VisibleSpan
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, provides advanced view manipulation functionality.
+ /// </summary>
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public abstract ITextView AdvancedTextView
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="Caret"/> of this view.
+ /// </summary>
+ public abstract Caret Caret
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="Selection"/>. of this view.
+ /// </summary>
+ public abstract Selection Selection
+ {
+ get;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, gets the <see cref="TextBuffer"/>. of this view.
+ /// </summary>
+ public abstract TextBuffer TextBuffer
+ {
+ get;
+ }
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs b/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs
new file mode 100644
index 0000000..0217538
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs
@@ -0,0 +1,16 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ public static class TrackChangesFormatDefinitions
+ {
+ public const string TrackChangesBeforeSave = "Track Changes before save";
+ public const string TrackChangesAfterSave = "Track Changes after save";
+ public const string TrackChangesReverted = "Track reverted changes";
+ }
+}
diff --git a/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs
new file mode 100644
index 0000000..adbb0ca
--- /dev/null
+++ b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs
@@ -0,0 +1,45 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Defines the meaning of the verticalOffset parameter in the <see cref="ITextView"/>.DisplayTextLineContaining(...).
+ /// </summary>
+ /// <remarks>
+ /// This enum adds a couple of modes to the ViewRelativePosition that we need to support for the InterLineAdornments
+ /// but don't want to expose. The WpfTextView accepts the new values but doesn't pass them on to anyone else.
+ /// </remarks>
+ public enum ViewRelativePosition2
+ {
+ /// <summary>
+ /// The offset with respect to the top of the view to the top of the line.
+ /// </summary>
+ /// <remarks>
+ /// Must match ViewRelativePosition.Top.
+ /// </remarks>
+ Top = ViewRelativePosition.Top,
+
+ /// <summary>
+ /// The offset with respect to the bottom of the view to the bottom of the line.
+ /// </summary>
+ /// <remarks>
+ /// Must match ViewRelativePosition.Bottom.
+ /// </remarks>
+ Bottom = ViewRelativePosition.Bottom,
+
+ /// <summary>
+ /// The offset with respect to the top of the view to the top of the text on the line.
+ /// </summary>
+ TextTop,
+
+ /// <summary>
+ /// The offset with respect to the bottom of the view to the bottom of the text on the line.
+ /// </summary>
+ TextBottom
+ }
+} \ No newline at end of file
diff --git a/src/Text/Def/TextData/Model/ITextBuffer2.cs b/src/Text/Def/TextData/Model/ITextBuffer2.cs
new file mode 100644
index 0000000..a755074
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextBuffer2.cs
@@ -0,0 +1,35 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// A mutable sequence of Unicode characters encoded using UTF-16.
+ /// Positions within the buffer are treated as a sequence of characters (starting at character zero) or
+ /// as a sequence of lines (starting at line zero). An empty buffer has a single line containing no characters.
+ /// </summary>
+ /// <remarks>Any <see cref="ITextBuffer"/> will be upcastable to an <see cref="ITextBuffer2"/>.</remarks>
+ public interface ITextBuffer2 : ITextBuffer
+ {
+ /// <summary>
+ /// Occurs when a non-empty <see cref="ITextEdit"/> is successfully applied.
+ /// This is raised on a background thread. Listeners are expected to schedule any expensive
+ /// work to be done asynchronously outside of this thread.
+ /// </summary>
+ /// <remarks>
+ /// Listeners of this event are not expected to modify the buffer. For performance reasons
+ /// handlers that modify the buffer should listen to <see cref="ITextBuffer.ChangedHighPriority"/> event
+ /// instead.
+ /// <para>
+ /// This event is raised after <see cref="ITextBuffer.ChangedHighPriority"/> event.
+ /// It's guaranteed that individual listeners receive the <see cref="ChangedOnBackground"/> events
+ /// in a synchronized way (never on more than one thread at a time) and in the order
+ /// the edits were applied.
+ /// </para>
+ /// </remarks>
+ event EventHandler<TextContentChangedEventArgs> ChangedOnBackground;
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs b/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs
new file mode 100644
index 0000000..cbd5830
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs
@@ -0,0 +1,71 @@
+//
+// 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 internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.IO;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The factory service for ordinary TextBuffers.
+ /// </summary>
+ /// <remarks><para>This is a MEF Component, and should be imported as follows:
+ /// <code>
+ /// [Import]
+ /// ITextBufferFactoryService factory = null;
+ /// </code>
+ /// </para>
+ /// <para>Any <see cref="ITextBufferFactoryService"/> will be upcastable to an <see cref="ITextBufferFactoryService3"/>.</para>
+ /// </remarks>
+ public interface ITextBufferFactoryService3 : ITextBufferFactoryService // Annoying: we need to maintain ITextBufferFactoryService2 for Roslyn, but it is internal (and has to stay there).
+ {
+ /// <summary>
+ /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it
+ /// with the given text contained in <paramref name="span"/>.
+ /// </summary>
+ /// <param name="span">The initial text to add.</param>
+ /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param>
+ /// <returns>
+ /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>.
+ /// </returns>
+ /// <exception cref="ArgumentNullException">Either <paramref name="span"/> or <paramref name="contentType"/> is null.</exception>
+ ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType);
+
+ /// <summary>
+ /// Creates an <see cref="ITextBuffer"/> with the given <paramref name="contentType"/> and populates it by
+ /// reading data from the specified TextReader.
+ /// </summary>
+ /// <param name="reader">The TextReader from which to read.</param>
+ /// <param name="contentType">The <paramref name="contentType"/> for the text contained in the new <see cref="ITextBuffer"/></param>
+ /// <param name="length">The length of the file backing the text reader, if known; otherwise -1.</param>
+ /// <param name="traceId">An optional identifier used in debug tracing.</param>
+ /// <returns>
+ /// An <see cref="ITextBuffer"/> object with the given TextReader and <paramref name="contentType"/>.
+ /// </returns>
+ /// <exception cref="ArgumentNullException"><paramref name="reader"/> is null.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="contentType"/> is null.</exception>
+ /// <remarks>
+ /// <para>The <paramref name="reader"/> is not closed by this operation.</para>
+ /// <para>The <paramref name="length"/> is used to help select a storage strategy for the text buffer.</para>
+ /// </remarks>
+ ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length = -1, string traceId = "");
+
+ /// <summary>
+ /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it
+ /// with the text contained in <paramref name="image"/>.
+ /// </summary>
+ /// <param name="image">The initial text of the buffer.</param>
+ /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param>
+ /// <returns>
+ /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>.
+ /// </returns>
+ /// <remarks>The new <see cref="ITextBuffer"/> will not inherit the version history of <paramref name="image"/>.</remarks>
+ /// <exception cref="ArgumentNullException">Either <paramref name="image"/> or <paramref name="contentType"/> is null.</exception>
+ ITextBuffer CreateTextBuffer(ITextImage image, IContentType contentType);
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextImage.cs b/src/Text/Def/TextData/Model/ITextImage.cs
new file mode 100644
index 0000000..59379d6
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextImage.cs
@@ -0,0 +1,174 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Provides read access to an immutable sequence of Unicode characters.
+ /// The first character in the sequence has index zero.
+ /// </summary>
+ public interface ITextImage
+ {
+ /// <summary>
+ /// The <see cref="ITextImageVersion"/> associated with this <see cref="ITextImage"/>.
+ /// </summary>
+ /// <remarks>
+ /// This will be null unless this <see cref="ITextImage"/> was created by a component that
+ /// manages the version information.</remarks>
+ ITextImageVersion Version { get; }
+
+ /// <summary>
+ /// Create a new <see cref="ITextImage"/> that is a clone of a subspan of this <see cref="ITextImage"/>.
+ /// </summary>
+ /// <remarks>
+ /// <para>The new <see cref="ITextImage"/> will not inherit the version or version history of this <see cref="ITextImage"/>.</para>
+ /// <para>The <see cref="ITextImage.Version"/> of the returned image will be null even the contents are identical to this.</para>/// </remarks>
+ ITextImage GetSubText(Span span);
+
+ /// <summary>
+ /// Gets the number of UTF-16 characters contained in the image.
+ /// </summary>
+ int Length { get; }
+
+ /// <summary>
+ /// Gets the positive number of lines in the image. An image whose <see cref="Length"/> is zero is considered to have one line.
+ /// </summary>
+ int LineCount { get; }
+
+ /// <summary>
+ /// Gets text from the image starting at the beginning of the span and having length equal to the length of the span.
+ /// </summary>
+ /// <param name="span">The span to return.</param>
+ /// <exception cref="ArgumentOutOfRangeException">The end of the span is greater than <see cref="Length"/>.</exception>
+ /// <returns>A non-null string.</returns>
+ string GetText(Span span);
+
+ /// <summary>
+ /// Converts a range of text to a character array.
+ /// </summary>
+ /// <param name="startIndex">
+ /// The starting index of the range of text.
+ /// </param>
+ /// <param name="length">
+ /// The length of the text.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than the length of the image, or
+ /// <paramref name="length"/> is less than zero, or <paramref name="startIndex"/> plus <paramref name="length"/> is greater than the length of the image.</exception>
+ /// <returns>The array of characters starting at <paramref name="startIndex"/> in the underlying <see cref="ITextBuffer"/> and extend to its end.</returns>
+ char[] ToCharArray(int startIndex, int length);
+
+ /// <summary>
+ /// Copies a range of text to a character array.
+ /// </summary>
+ /// <param name="sourceIndex">
+ /// The starting index in the text image.
+ /// </param>
+ /// <param name="destination">
+ /// The destination array.
+ /// </param>
+ /// <param name="destinationIndex">
+ /// The index in the destination array at which to start copying the text.
+ /// </param>
+ /// <param name="count">
+ /// The number of characters to copy.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="sourceIndex"/> is less than zero or greater than the length of the image, or
+ /// <paramref name="count"/> is less than zero, or <paramref name="sourceIndex"/> + <paramref name="count"/> is greater than the length of the image, or
+ /// <paramref name="destinationIndex"/> is less than zero, or <paramref name="destinationIndex"/> plus <paramref name="count"/> is greater than the length of <paramref name="destination"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception>
+ void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
+
+ /// <summary>
+ /// Gets a single character at the specified position.
+ /// </summary>
+ /// <param name="position">The position of the character.</param>
+ /// <returns>The character at <paramref name="position"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than or equal to the length of the image.</exception>
+ char this[int position] { get; }
+
+ /// <summary>
+ /// Gets an <see cref="TextImageLine"/> for the given line number.
+ /// </summary>
+ /// <param name="lineNumber">The line number.</param>
+ /// <returns>A non-null <see cref="TextImageLine"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero or greater than or equal to <see cref="LineCount"/>.</exception>
+ TextImageLine GetLineFromLineNumber(int lineNumber);
+
+ /// <summary>
+ /// Gets an <see cref="TextImageLine"/> for a line at the given position.
+ /// </summary>
+ /// <param name="position">The position.</param>
+ /// <returns>A non-null <see cref="TextImageLine"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than length of line.</exception>
+ TextImageLine GetLineFromPosition(int position);
+
+ /// <summary>
+ /// Gets the number of the line that contains the character at the specified position.
+ /// </summary>
+ /// <returns>The line number of the line in which <paramref name="position"/> lies.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than Length/>.</exception>
+ int GetLineNumberFromPosition(int position);
+
+ /// <summary>
+ /// Writes a substring of the contents of the image.
+ /// </summary>
+ /// <param name="writer">The <see cref="System.IO.TextWriter"/> to use.</param>
+ /// <param name="span">The span of text to write.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">The end of the span is greater than the length of the image.
+ /// </exception>
+ void Write(System.IO.TextWriter writer, Span span);
+ }
+
+ public static class TextImageExtensions
+ {
+ /// <summary>
+ /// Gets text from the image starting at <paramref name="startIndex"/> and having length equal to <paramref name="length"/>.
+ /// </summary>
+ /// <param name="startIndex">The starting index.</param>
+ /// <param name="length">The length of text to get.</param>
+ /// <returns>The string of length <paramref name="length"/> starting at <paramref name="startIndex"/> in the underlying <see cref="ITextBuffer"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than the length of the image,
+ /// or <paramref name="length"/> is less than zero, or <paramref name="startIndex"/> plus <paramref name="length"/> is greater than the length of the image.</exception>
+ public static string GetText(this ITextImage image, int startIndex, int length)
+ {
+ return image.GetText(new Span(startIndex, length));
+ }
+
+ /// <summary>
+ /// Gets all the text in the image.
+ /// </summary>
+ /// <returns>A non-null string.</returns>
+ /// <remarks>Caveat emptor. Calling GetText() on a 100MB <see cref="ITextImage"/> will give you exactly what you asked for, which
+ /// probably isn't what you wanted.</remarks>
+ public static string GetText(this ITextImage image)
+ {
+ return image.GetText(new Span(0, image.Length));
+ }
+
+ /// <summary>
+ /// Create a new <see cref="ITextImage"/> that is a clone of a subspan of this <see cref="ITextImage"/>.
+ /// </summary>
+ /// <remarks>
+ /// The new <see cref="ITextImage"/> will not inherit the version or version history of this <see cref="ITextImage"/>.</remarks>
+ public static ITextImage GetSubText(this ITextImage image, int startIndex, int length)
+ {
+ return image.GetSubText(new Span(startIndex, length));
+ }
+
+ /// <summary>
+ /// Writes the contents of the image.
+ /// </summary>
+ /// <param name="writer">The <see cref="System.IO.TextWriter"/>to use.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
+ public static void Write(this ITextImage image, System.IO.TextWriter writer)
+ {
+ image.Write(writer, new Span(0, image.Length));
+ }
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextImageFactoryService.cs b/src/Text/Def/TextData/Model/ITextImageFactoryService.cs
new file mode 100644
index 0000000..797f7b1
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextImageFactoryService.cs
@@ -0,0 +1,35 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.IO;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The factory service for creating <see cref="ITextImage"/>s.
+ /// </summary>
+ /// <remarks>This is a MEF Component, and should be imported as follows:
+ /// <code>
+ /// [Import]
+ /// ITextImageFactoryService factory = null;
+ /// </code>
+ /// </remarks>
+ public interface ITextImageFactoryService
+ {
+ /// <summary>
+ /// Create an <see cref="ITextImage"/> that contains <paramref name="text"/>.
+ /// </summary>
+ ITextImage CreateTextImage(string text);
+
+ /// <summary>
+ /// Create an <see cref="ITextImage"/> that contains the contents of <paramref name="reader"/>.
+ /// </summary>
+ /// <param name="length">An estimate of the total length of the file. -1 if the file size is unknown.</param>
+ /// <remarks>
+ /// <para>The <paramref name="length"/> is used to decide whether or not to attempt to compress the text read from <paramref name="reader"/>. It does not need to be accurate.</para></remarks>
+ ITextImage CreateTextImage(TextReader reader, long length = -1);
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextImageVersion.cs b/src/Text/Def/TextData/Model/ITextImageVersion.cs
new file mode 100644
index 0000000..bbf36e6
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextImageVersion.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// Describes a version of an <see cref="ITextImage"/>.
+ /// </summary>
+ public interface ITextImageVersion
+ {
+ /// <summary>
+ /// Gets the next <see cref="ITextImageVersion"/>. Returns null if and only if this is the most recent version of its <see cref="ITextImage"/>.
+ /// </summary>
+ ITextImageVersion Next { get; }
+
+ /// <summary>
+ /// Gets the length in characters of this <see cref="ITextImageVersion"/>.
+ /// </summary>
+ int Length { get; }
+
+ /// <summary>
+ /// Gets the text changes that produce the next version. Returns null if and only if this is the most recent version of its <see cref="ITextImage"/>.
+ /// </summary>
+ INormalizedTextChangeCollection Changes { get; }
+
+ /// <summary>
+ /// The version number for this version.
+ /// </summary>
+ /// <remarks>This starts at zero and is increased as new <see cref="ITextImage"/>s that are based on this <see cref="ITextImage"/> are created.</remarks>
+ int VersionNumber { get; }
+
+ /// <summary>
+ /// Gets the oldest version number for which all text changes between that version and this version have
+ /// been canceled out by corresponding undo/redo operations.
+ /// </summary>
+ /// <remarks>
+ /// If ReiteratedVersionNumber is not equal to <see cref="VersionNumber" />, then for every
+ /// <see cref="ITextChange" /> not originated by an undo operation between ReiteratedVersionNumber and VersionNumber, there is a
+ /// corresponding <see cref="ITextChange"/> originated by an undo operation that cancels it out. So the contents of the two
+ /// versions are necessarily identical.
+ ///<para>
+ /// Setting this property correctly is the responsibility of the undo system; aside from this
+ /// property, the text buffer and related classes are unaware of undo and redo.
+ /// </para>
+ /// <para>
+ /// Note that the <see cref="ITextImageVersion"/> created through through an operation that makes no text changes
+ /// will therefore have the ReiteratedVersionNumber of the previous version.
+ /// </para>
+ /// </remarks>
+ int ReiteratedVersionNumber { get; }
+
+ /// <summary>
+ /// A unique identifier associated with a version and all versions derived from it.
+ /// </summary>
+ object Identifier { get; }
+
+ /// <summary>
+ /// Translate a position in another <see cref="ITextImageVersion"/> to this <see cref="ITextImageVersion"/>.
+ /// </summary>
+ int TrackTo(VersionedPosition other, PointTrackingMode mode);
+
+ /// <summary>
+ /// Translate a span in another <see cref="ITextImageVersion"/> to this <see cref="ITextImageVersion"/>.
+ /// </summary>
+ Span TrackTo(VersionedSpan span, SpanTrackingMode mode);
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextSnapshot2.cs b/src/Text/Def/TextData/Model/ITextSnapshot2.cs
new file mode 100644
index 0000000..0979093
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextSnapshot2.cs
@@ -0,0 +1,32 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Provides read access to an immutable snapshot of a <see cref="ITextBuffer"/> containing a sequence of Unicode characters.
+ /// The first character in the sequence has index zero.
+ /// </summary>
+ /// <remarks>Any <see cref="ITextSnapshot"/> will be upcastable to an <see cref="ITextSnapshot2"/>.</remarks>
+ public interface ITextSnapshot2 : ITextSnapshot
+ {
+ /// <summary>
+ /// Gets the underlying <see cref="ITextImage"/> of the snapshot.
+ /// </summary>
+ ITextImage TextImage { get; }
+
+ /// <summary>
+ /// Save the entire snapshot to the specified <paramref name="filePath"/>.
+ /// </summary>
+ /// <param name="filePath">Path to save</param>
+ /// <param name="replaceFile">If true, replace an exising file.</param>
+ /// <param name="encoding">Encoding to use to save the file.</param>
+ void SaveToFile(string filePath, bool replaceFile, Encoding encoding);
+ }
+}
diff --git a/src/Text/Def/TextData/Model/ITextVersion2.cs b/src/Text/Def/TextData/Model/ITextVersion2.cs
new file mode 100644
index 0000000..75dedc8
--- /dev/null
+++ b/src/Text/Def/TextData/Model/ITextVersion2.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// Describes a version of an <see cref="ITextBuffer"/>. Each application of an <see cref="ITextEdit"/> to a text buffer
+ /// generates a new <see cref="ITextVersion"/>.
+ /// </summary>
+ /// <remarks>Any <see cref="ITextVersion"/> will be upcastable to an <see cref="ITextVersion2"/>.</remarks>
+ public interface ITextVersion2 : ITextVersion
+ {
+ ITextImageVersion ImageVersion { get; }
+ }
+}
diff --git a/src/Text/Def/TextData/Model/TextImageLine.cs b/src/Text/Def/TextData/Model/TextImageLine.cs
new file mode 100644
index 0000000..b7e95c1
--- /dev/null
+++ b/src/Text/Def/TextData/Model/TextImageLine.cs
@@ -0,0 +1,147 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// Immutable information about a line of text from an <see cref="ITextImage"/>.
+ /// </summary>
+ public struct TextImageLine : IEquatable<TextImageLine>
+ {
+ public readonly static TextImageLine Invalid = new TextImageLine();
+
+ public TextImageLine(ITextImage image, int lineNumber, Span extent, int lineBreakLength)
+ {
+ if (image == null)
+ throw new ArgumentNullException(nameof(image));
+
+ if ((lineNumber < 0) || (lineNumber >= image.LineCount))
+ throw new ArgumentOutOfRangeException(nameof(lineNumber));
+
+ if (extent.End > image.Length)
+ throw new ArgumentOutOfRangeException(nameof(extent));
+
+ if ((lineBreakLength < 0) || (lineBreakLength > 2) || (extent.End + lineBreakLength > image.Length))
+ throw new ArgumentOutOfRangeException(nameof(lineBreakLength));
+
+ this.Image = image;
+ this.LineNumber = lineNumber;
+ this.Extent = extent;
+ this.LineBreakLength = lineBreakLength;
+ }
+
+ /// <summary>
+ /// The <see cref="ITextImage"/> in which the line appears.
+ /// </summary>
+ public readonly ITextImage Image;
+
+ /// <summary>
+ /// The extent of the line, excluding any line break characters.
+ /// </summary>
+ public readonly Span Extent;
+
+ /// <summary>
+ /// The extent of the line, including any line break characters.
+ /// </summary>
+ public Span ExtentIncludingLineBreak { get { return new Span(this.Extent.Start, this.LengthIncludingLineBreak); } }
+
+ /// <summary>
+ /// The 0-origin line number of the line.
+ /// </summary>
+ public readonly int LineNumber;
+
+ /// <summary>
+ /// The position of the first character in the line.
+ /// </summary>
+ public int Start { get { return this.Extent.Start; } }
+
+ /// <summary>
+ /// Length of the line, excluding any line break characters.
+ /// </summary>
+ public int Length { get { return this.Extent.Length; } }
+
+ /// <summary>
+ /// Length of the line, including any line break characters.
+ /// </summary>
+ public int LengthIncludingLineBreak { get { return this.Extent.Length + this.LineBreakLength; } }
+
+ /// <summary>
+ /// The position of the first character past the end of the line, excluding any
+ /// line break characters (thus will address a line break character, except
+ /// for the last line in the buffer, in which case it addresses a
+ /// position past the end of the buffer).
+ /// </summary>
+ public int End { get { return this.Extent.End; } }
+
+ /// <summary>
+ /// The position of the first character past the end of the line, including any
+ /// line break characters (thus will address the first character in
+ /// the succeeding line, unless this is the last line, in which case it addresses a
+ /// position past the end of the buffer).
+ /// </summary>
+ public int EndIncludingLineBreak { get { return this.Extent.End + this.LineBreakLength; } }
+
+ /// <summary>
+ /// Length of line break characters (always falls in the range [0..2]).
+ /// </summary>
+ public readonly int LineBreakLength;
+
+ /// <summary>
+ /// The text of the line, excluding any line break characters.
+ /// </summary>
+ public string GetText() { return this.Image.GetText(this.Extent); }
+
+ /// <summary>
+ /// The text of the line, including any line break characters.
+ /// </summary>
+ public string GetTextIncludingLineBreak() { return this.Image.GetText(this.ExtentIncludingLineBreak); }
+
+ /// <summary>
+ /// The string consisting of the line break characters (if any) at the
+ /// end of the line. Has zero length for the last line in the buffer.
+ /// </summary>
+ public string GetLineBreakText() { return this.Image.GetText(new Span(this.Extent.End, this.LineBreakLength)); }
+
+ public override int GetHashCode()
+ {
+ return (this.Image != null) ? (this.LineNumber ^ this.Image.GetHashCode()) : 0;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is TextImageLine)
+ {
+ var other = (TextImageLine)obj;
+ return this.Equals(other);
+ }
+
+ return false;
+ }
+
+ public bool Equals(TextImageLine other)
+ {
+ return (other.Image == this.Image) && (other.LineNumber == this.LineNumber);
+ }
+
+ public static bool operator ==(TextImageLine left, TextImageLine right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(TextImageLine left, TextImageLine right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return (this.Image == null)
+ ? nameof(Invalid)
+ : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{0}[{1}, {2}+{3}]",
+ this.Image.Version?.VersionNumber, this.Extent.Start, this.Extent.End, this.LineBreakLength);
+ }
+ }
+}
diff --git a/src/Text/Def/TextData/Model/VersionedPosition.cs b/src/Text/Def/TextData/Model/VersionedPosition.cs
new file mode 100644
index 0000000..8f9e036
--- /dev/null
+++ b/src/Text/Def/TextData/Model/VersionedPosition.cs
@@ -0,0 +1,90 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// Describes a location in a specific <see cref="ITextImageVersion"/>.
+ /// </summary>
+ public struct VersionedPosition : IEquatable<VersionedPosition>
+ {
+ public readonly ITextImageVersion Version;
+ public readonly int Position;
+
+ public readonly static VersionedPosition Invalid = new VersionedPosition();
+
+ public VersionedPosition(ITextImageVersion version, int position)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ if ((position < 0) || (position > version.Length))
+ {
+ throw new ArgumentOutOfRangeException(nameof(position));
+ }
+
+ this.Version = version;
+ this.Position = position;
+ }
+
+ public static implicit operator int(VersionedPosition position)
+ {
+ return position.Position;
+ }
+
+ public VersionedPosition TranslateTo(ITextImageVersion other, PointTrackingMode mode)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+
+ return new VersionedPosition(other, other.TrackTo(this, mode));
+ }
+
+ public override int GetHashCode()
+ {
+ return (this.Version != null) ? (this.Position ^ this.Version.GetHashCode()) : 0;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is VersionedPosition)
+ {
+ var other = (VersionedPosition)obj;
+ return this.Equals(other);
+ }
+
+ return false;
+ }
+
+ public bool Equals(VersionedPosition other)
+ {
+ return (other.Version == this.Version) && (other.Position == this.Position);
+ }
+
+ public static bool operator ==(VersionedPosition left, VersionedPosition right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(VersionedPosition left, VersionedPosition right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return (this.Version == null)
+ ? nameof(Invalid)
+ : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}",
+ this.Version.VersionNumber,
+ this.Position);
+ }
+ }
+}
diff --git a/src/Text/Def/TextData/Model/VersionedSpan.cs b/src/Text/Def/TextData/Model/VersionedSpan.cs
new file mode 100644
index 0000000..67de693
--- /dev/null
+++ b/src/Text/Def/TextData/Model/VersionedSpan.cs
@@ -0,0 +1,90 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text
+{
+ using System;
+
+ /// <summary>
+ /// Describes a span in a specific <see cref="ITextImageVersion"/>.
+ /// </summary>
+ public struct VersionedSpan : IEquatable<VersionedSpan>
+ {
+ public readonly ITextImageVersion Version;
+ public readonly Span Span;
+
+ public readonly static VersionedSpan Invalid = new VersionedSpan();
+
+ public VersionedSpan(ITextImageVersion version, Span span)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ if (span.End > version.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(span));
+ }
+
+ this.Version = version;
+ this.Span = span;
+ }
+
+ public static implicit operator Span(VersionedSpan span)
+ {
+ return span.Span;
+ }
+
+ public VersionedSpan TranslateTo(ITextImageVersion other, SpanTrackingMode mode)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+
+ return new VersionedSpan(other, other.TrackTo(this, mode));
+ }
+
+ public override int GetHashCode()
+ {
+ return (this.Version != null) ? (this.Span.GetHashCode() ^ this.Version.GetHashCode()) : 0;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is VersionedSpan)
+ {
+ var other = (VersionedSpan)obj;
+ return this.Equals(other);
+ }
+
+ return false;
+ }
+
+ public bool Equals(VersionedSpan other)
+ {
+ return (other.Version == this.Version) && (other.Span == this.Span);
+ }
+
+ public static bool operator ==(VersionedSpan left, VersionedSpan right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(VersionedSpan left, VersionedSpan right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return (this.Version == null)
+ ? nameof(Invalid)
+ : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}",
+ this.Version.VersionNumber,
+ this.Span);
+ }
+ }
+}
diff --git a/src/Text/Def/TextUI/Adornments/BlockContext.cs b/src/Text/Def/TextUI/Adornments/BlockContext.cs
deleted file mode 100644
index b74d188..0000000
--- a/src/Text/Def/TextUI/Adornments/BlockContext.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using Microsoft.VisualStudio.Text.Adornments;
-using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.Text.Tagging;
-using System;
-
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// An implementation of <see cref="IBlockContext"/>.
- /// </summary>
- public class BlockContext : IBlockContext
- {
- private readonly IBlockTag _blockTag;
- private readonly ITextView _view;
- private readonly object _content;
- /// <summary>
- /// Initializes a new instance of <see cref="IBlockContext"/> with the specified block tag.
- /// </summary>
- /// <param name="blockTag">The block tag associated with the structural block.</param>
- /// <param name="view">The text view associated with the structural block.</param>
- /// <param name="content">The content, including hiearchical parent statements, to be displayed in the tooltip.</param>
- public BlockContext(IBlockTag blockTag, ITextView view, object content)
- {
- if (blockTag == null)
- throw new ArgumentNullException("blockTag");
-
- if (view == null)
- throw new ArgumentNullException("view");
-
- if (content == null)
- throw new ArgumentNullException("content");
-
- _blockTag = blockTag;
- _view = view;
- _content = content;
- }
-
- /// <summary>
- /// Gets the block tag associated with this context.
- /// </summary>
- public IBlockTag BlockTag
- {
- get { return _blockTag; }
- }
-
- /// <summary>
- /// Gets the text view associated with this context.
- /// </summary>
- public ITextView TextView
- {
- get { return _view; }
- }
-
- /// <summary>
- /// Gets the content that will be displayed in the tool tip, including hierarchical parent content.
- /// </summary>
- public object Content
- {
- get { return _content; }
- }
- }
-}
diff --git a/src/Text/Def/TextUI/Adornments/IBlockContext.cs b/src/Text/Def/TextUI/Adornments/IBlockContext.cs
deleted file mode 100644
index 229eaf1..0000000
--- a/src/Text/Def/TextUI/Adornments/IBlockContext.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.Text.Tagging;
-
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// Defines the block context used to display structural block information.
- /// </summary>
- public interface IBlockContext
- {
- /// <summary>
- /// Gets the content that will be displayed in the tool tip, including hierarchical parent content.
- /// </summary>
- object Content { get; }
-
- /// <summary>
- /// Gets the block tag associated with this context.
- /// </summary>
- IBlockTag BlockTag { get; }
-
- /// <summary>
- /// Gets the text view associated with this context.
- /// </summary>
- ITextView TextView { get; }
- }
-}
diff --git a/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs b/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs
deleted file mode 100644
index 674881a..0000000
--- a/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// Creates a <see cref="IBlockContextSource"/> for a given buffer.
- /// </summary>
- /// <remarks>This is a MEF component part, and should be exported as follows:
- /// [Export(typeof(IBlockContextProvider))]
- /// Component exporters must add the Name and Order attribute to define the order of the provider in the provider chain.
- /// </remarks>
- public interface IBlockContextProvider
- {
- /// <summary>
- /// Creates a block context source for the given text buffer.
- /// </summary>
- /// <param name="textBuffer">The text buffer for which to create a provider.</param>
- /// <param name="token">The cancelation token for this asynchronous method call.</param>
- /// <returns>A valid <see cref="IBlockContextSource" /> instance, or null if none could be created.</returns>
- Task<IBlockContextSource> TryCreateBlockContextSourceAsync(ITextBuffer textBuffer, CancellationToken token);
- }
-}
diff --git a/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs b/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs
deleted file mode 100644
index bcb2bb5..0000000
--- a/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.Text.Tagging;
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// Provides content for structural block tool tips for a given <see cref="IBlockTag"/>.
- /// </summary>
- public interface IBlockContextSource : IDisposable
- {
- /// <summary>
- /// Gets the contexts for the given block tag.
- /// </summary>
- /// <param name="blockTag">The block tag for which the context is requested.</param>
- /// <param name="view">The text view associated with the current context.</param>
- /// <param name="token">The cancellation token for this asynchronous method call.</param>
- /// <returns>The <see cref="IBlockContext" /> to be displayed in the tool tip.</returns>
- Task<IBlockContext> GetBlockContextAsync(IBlockTag blockTag, ITextView view, CancellationToken token);
- }
-}
diff --git a/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs b/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs
new file mode 100644
index 0000000..8f5688e
--- /dev/null
+++ b/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs
@@ -0,0 +1,32 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+
+namespace Microsoft.VisualStudio.Text.Adornments
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Text.UI.Adornments;
+
+ /// <summary>
+ /// Provides context for structural block tool tips for a given sequence
+ /// of <see cref="IStructureElement"/>s.
+ /// </summary>
+ public interface IStructureContextSource : IDisposable
+ {
+ /// <summary>
+ /// Gets the context for the given structure tags.
+ /// </summary>
+ /// <param name="elements">The structure tags to get context for.</param>
+ /// <param name="token">The cancellation token for this asynchronous method call.</param>
+ /// <returns>The object to be displayed in the structure tool tip.</returns>
+ /// <remarks>
+ /// If the object returned by this method implements ITextView, ITextView.Close() is called
+ /// when the tooltip is dismissed.
+ /// </remarks>
+ Task<object> GetStructureContextAsync(IEnumerable<IStructureElement> elements, CancellationToken token);
+ }
+}
diff --git a/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs b/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs
new file mode 100644
index 0000000..1807cb8
--- /dev/null
+++ b/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+
+namespace Microsoft.VisualStudio.Text.Adornments
+{
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// Creates an <see cref="IStructureContextSource"/> for a given buffer.
+ /// </summary>
+ /// <remarks>This is a MEF component part, and should be exported as follows:
+ /// [Export(typeof(IStructureContextSourceProvider))]
+ /// [Name("MyProviderName")]
+ /// [ContentType("MyContentTypeName")]
+ /// [Order(Before = "Foo", After = "Bar")]
+ /// Component exporters must add the Name and Order attribute to define the order of the provider in the provider chain.
+ /// </remarks>
+ public interface IStructureContextSourceProvider
+ {
+ /// <summary>
+ /// Creates a structure context source for the given text view.
+ /// </summary>
+ /// <param name="textView">The text view for which to create an <see cref="IStructureContextSource"/>.</param>
+ /// <returns>A valid <see cref="IStructureContextSource" /> instance, or null if none could be created.</returns>
+ IStructureContextSource CreateStructureContextSource(ITextView textView);
+ }
+}
diff --git a/src/Text/Def/TextUI/Adornments/IStructureElement.cs b/src/Text/Def/TextUI/Adornments/IStructureElement.cs
new file mode 100644
index 0000000..06fd06f
--- /dev/null
+++ b/src/Text/Def/TextUI/Adornments/IStructureElement.cs
@@ -0,0 +1,84 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+
+namespace Microsoft.VisualStudio.Text.UI.Adornments
+{
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Adornments;
+
+ /// <summary>
+ /// Represents a unit of structure in a code <see cref="ITextBuffer"/>.
+ /// </summary>
+ /// <remarks>
+ /// This class is an immutable subtree of the code structure in the view.
+ /// </remarks>
+ public interface IStructureElement
+ {
+ /// <summary>
+ /// The <see cref="IStructureElement"/>s nested within this one.
+ /// </summary>
+ IReadOnlyList<IStructureElement> Children { get; }
+
+ /// <summary>
+ /// The span of text at the top of the structural element. e.g.: 'if (true)'
+ /// </summary>
+ SnapshotSpan? HeaderSpan { get; }
+
+ /// <summary>
+ /// The vertical span within which the structure guide line adornment should
+ /// be drawn.
+ /// </summary>
+ SnapshotSpan? GuideLineSpan { get; }
+
+ /// <summary>
+ /// The span of text to collapse when the outlining adornment is invoked.
+ /// </summary>
+ SnapshotSpan? OutliningSpan { get; }
+
+ /// <summary>
+ /// The full extent of the block, from the start of the header to the end of the guideline.
+ /// </summary>
+ SnapshotSpan ExtentSpan { get; }
+
+ /// <summary>
+ /// The horizontal offset with which to align the structure guide line.
+ /// </summary>
+ SnapshotPoint? GuideLineHorizontalAnchorPoint { get; }
+
+ /// <summary>
+ /// One of the <see cref="PredefinedStructureTypes"/>, indicating the semantics
+ /// of this structural element.
+ /// </summary>
+ string Type { get; }
+
+ /// <summary>
+ /// Indicates whether or not this element should display a collapse adornment.
+ /// </summary>
+ bool IsCollapsible { get; }
+
+ /// <summary>
+ /// Indicates whether or not this element should be collapsed at document open.
+ /// </summary>
+ bool IsDefaultCollapsed { get; }
+
+ /// <summary>
+ /// Indicates whether or not this element is related to implementation of a method,
+ /// function, or property.
+ /// </summary>
+ bool IsImplementation { get; }
+
+ /// <summary>
+ /// Gets the text to display in the collapse adornment.
+ /// </summary>
+ /// <returns>The text to display in the collapse adornment.</returns>
+ object GetCollapsedForm();
+
+ /// <summary>
+ /// Gets the text to display in the collapse adornment tooltip.
+ /// </summary>
+ /// <returns>The text displayed in the collapse adornment tooltip.</returns>
+ object GetCollapsedHintForm();
+ }
+}
diff --git a/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs b/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs
new file mode 100644
index 0000000..5260fb6
--- /dev/null
+++ b/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs
@@ -0,0 +1,73 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Adornments
+{
+ /// <summary>
+ /// Enumerates the predefined structural block types.
+ /// </summary>
+ public static class PredefinedStructureTagTypes
+ {
+ /// <summary>
+ /// Represents structural blocks, with vertical line adornments displayed.
+ /// </summary>
+ public const string Structural = nameof(Structural);
+
+ /// <summary>
+ /// Represents non-structural blocks, with no vertical line adornments
+ /// displayed, only expand and collapse.
+ /// </summary>
+ public const string Nonstructural = nameof(Nonstructural);
+
+ /// <summary>
+ /// Represents a code comment, with vertical line adornments.
+ /// </summary>
+ public const string Comment = nameof(Comment);
+
+ /// <summary>
+ /// Represents a PreprocessorRegion, with vertical line adornments.
+ /// </summary>
+ public const string PreprocessorRegion = nameof(PreprocessorRegion);
+
+ /// <summary>
+ /// Represents an Import or Imports Block, with vertical line adornments.
+ /// </summary>
+ public const string Imports = nameof(Imports);
+
+ /// <summary>
+ /// Represents a Namespace, with vertical line adornments.
+ /// </summary>
+ public const string Namespace = nameof(Namespace);
+
+ /// <summary>
+ /// Represents a Type, with vertical line adornments.
+ /// </summary>
+ public const string Type = nameof(Type);
+
+ /// <summary>
+ /// Represents a class Member, such as a method or property, with vertical line adornments.
+ /// </summary>
+ public const string Member = nameof(Member);
+
+ /// <summary>
+ /// Represents a Statement, with vertical line adornments.
+ /// </summary>
+ public const string Statement = nameof(Statement);
+
+ /// <summary>
+ /// Represents a Conditional, with vertical line adornments.
+ /// </summary>
+ public const string Conditional = nameof(Conditional);
+
+ /// <summary>
+ /// Represents a Loop, with vertical line adornments.
+ /// </summary>
+ public const string Loop = nameof(Loop);
+
+ /// <summary>
+ /// Represents an Expression, with vertical line adornments.
+ /// </summary>
+ public const string Expression = nameof(Expression);
+ }
+}
diff --git a/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs b/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs
deleted file mode 100644
index b1146ae..0000000
--- a/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// Enumerates the predefined structural block types.
- /// </summary>
- public static class PredefinedStructureTypes
- {
- /// <summary>
- /// Represents structural blocks, with vertical line adornments displayed.
- /// </summary>
- public const string Structural = "Structural";
-
- /// <summary>
- /// Represents non-structural blocks, with no vertical line adornments
- /// displayed, only expand and collapse.
- /// </summary>
- public const string Nonstructural = "Nonstructural";
-
- public const string PropertyBlock = "PropertyBlock";
- public const string AccessorBlock = "AccessorBlock";
- public const string AnonymousMethodBlock = "AnonymousMethodBlock"; // i.e. lambda bodies
- public const string Constructor = "Constructor";
- public const string Destructor = "Destructor";
- public const string Operator = "Operator";
- public const string Method = "Method";
- public const string Namespace = "Namespace";
- public const string Class = "Class";
- public const string Interface = "Interface";
- public const string Struct = "Struct";
- public const string TryCatchFinally = "TryCatchFinally";
- public const string Conditional = "Conditional"; // i.e. If statements, ‘switch’ statements.i.e conditionals+branches
- public const string Case = "Case";
- public const string Loop = "Loop"; // i.e. While/For/Foreach/Do/Until (i.e.loops)
- public const string Standalone = "Standalone"; // i.e. stand-alone {} used for scoping.
-
- public const string Lock = "Context"; // i.e. using/lock/checked/unchecked. Need a better name for this.
-
- public const string Unknown = "Unknown";
- }
-}
diff --git a/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs b/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs
deleted file mode 100644
index f71fe11..0000000
--- a/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Windows.Media;
-
-namespace Microsoft.VisualStudio.Text.Adornments
-{
- /// <summary>
- /// Defines a set of properties that will be used to style the default structural block tool tip.
- /// </summary>
- /// <remarks>
- /// This is a MEF component part, and should be exported with the following attributes:
- /// [Export(typeof(StructureAdornmentStyle))]
- /// [Name]
- /// [Order]
- /// </remarks>
- public class StructureAdornmentStyle
- {
- /// <summary>
- /// Gets a <see cref="Brush"/> that will be used to paint the borders in the structure block tool tip.
- /// </summary>
- public virtual Brush BorderBrush { get; protected set; }
-
- /// <summary>
- /// Gets a <see cref="Brush"/> that will be used to paint the background of the structure block tool tip.
- /// </summary>
- public virtual Brush BackgroundBrush { get; protected set; }
- }
-}
diff --git a/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs b/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs
new file mode 100644
index 0000000..6067159
--- /dev/null
+++ b/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs
@@ -0,0 +1,34 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Differencing
+{
+
+ /// <summary>
+ /// The highlight mode for this <see cref="IDifferenceViewer"/>.
+ /// </summary>
+ public enum DifferenceHighlightMode3
+ {
+ /// <summary>
+ /// In this mode, line differences should be displayed only to the last character on each line.
+ /// </summary>
+ CodeContour = DifferenceHighlightMode.CodeContour,
+
+ /// <summary>
+ /// In this mode, line differences should be displayed so that they take up the entire width of the viewport.
+ /// </summary>
+ WholeLine = DifferenceHighlightMode.WholeLine,
+
+ /// <summary>
+ /// In this mode, line and word differences are shown as outlined rectangles.
+ /// </summary>
+ BlockOutline = DifferenceHighlightMode2.BlockOutline,
+
+ /// <summary>
+ /// In this mode, line differences are shown as outlined rectangles and
+ /// word differences are shown as colored rectangles.
+ /// </summary>
+ BlockOutlineWithWordDiffs
+ }
+}
diff --git a/src/Text/Def/TextUI/Editor/ConnectionReason.cs b/src/Text/Def/TextUI/Editor/ConnectionReason.cs
new file mode 100644
index 0000000..194842a
--- /dev/null
+++ b/src/Text/Def/TextUI/Editor/ConnectionReason.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Defines the reasons for connecting or disconnecting a text buffer and a text view.
+ /// </summary>
+ public enum ConnectionReason
+ {
+ /// <summary>
+ /// The <see cref="ITextView"/> has been opened or closed.
+ /// </summary>
+ TextViewLifetime,
+
+ /// <summary>
+ /// The <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> of the subject buffer has changed.
+ /// </summary>
+ ContentTypeChange,
+
+ /// <summary>
+ /// A buffer has been added to or removed from <see cref="Microsoft.VisualStudio.Text.Projection.IBufferGraph"/>.
+ /// </summary>
+ BufferGraphChange
+ }
+}
diff --git a/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs b/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs
new file mode 100644
index 0000000..4390964
--- /dev/null
+++ b/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ using System.Collections.ObjectModel;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// Listens to text buffers of a particular content type to find out when they are opened or closed
+ /// in the text editor.
+ /// </summary>
+ /// <remarks>This is a MEF component part, and should be exported with the following attribute:
+ /// [Export(typeof(ITextViewConnectionListener))]
+ /// [ContentType("...")]
+ /// [TextViewRole("...")]
+ /// </remarks>
+ public interface ITextViewConnectionListener
+ {
+ /// <summary>
+ /// Called when one or more <see cref="ITextBuffer"/> objects of the appropriate <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> are connected to a <see cref="ITextView"/>.
+ /// </summary>
+ /// <remarks>
+ /// A connection can occur at one of three times: (1) when the view is first created; (2) when the buffer becomes a member of the
+ /// <see cref="Microsoft.VisualStudio.Text.Projection.IBufferGraph"/> for the view; or (3) when the
+ /// <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> of the buffer changes.
+ /// </remarks>
+ /// <param name="textView">The <see cref="ITextView"/> to which the subject buffers are being connected.</param>
+ /// <param name="reason">The cause of the connection.</param>
+ /// <param name="subjectBuffers">The non-empty list of <see cref="ITextBuffer"/> objects with matching
+ /// content types.</param>
+ void SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers);
+
+ /// <summary>
+ /// Called when one or more <see cref="ITextBuffer"/> objects no longer satisfy the conditions for being included in the subject buffers.
+ /// </summary>
+ /// <remarks>
+ /// Text buffers can be disconnected when they are removed as source buffers of some projection buffer,
+ /// or when their content type changes, or when the <see cref="ITextView"/> is closed.
+ /// </remarks>
+ /// <param name="textView">The <see cref="ITextView"/> from which the subject buffers are being disconnected.</param>
+ /// <param name="reason">The cause of the disconnection.</param>
+ /// <param name="subjectBuffers">The non-empty list of <see cref="ITextBuffer"/> objects.</param>
+ void SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers);
+ }
+}
diff --git a/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs b/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs
new file mode 100644
index 0000000..b6427b7
--- /dev/null
+++ b/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Listens for when <see cref="ITextView"/>s are created.
+ /// </summary>
+ /// <remarks>This is a MEF component part, and should be exported with the following attribute:
+ /// [Export(typeof(ITextViewCreationListener))]
+ /// [ContentType("...")]
+ /// [TextViewRole("...")]
+ /// </remarks>
+ public interface ITextViewCreationListener
+ {
+ /// <summary>
+ /// Called when a text view having matching roles is created over a text data model having a matching content type.
+ /// </summary>
+ /// <param name="textView">The newly created text view.</param>
+ void TextViewCreated(ITextView textView);
+ }
+}
diff --git a/src/Text/Def/TextUI/Tags/BlockTag.cs b/src/Text/Def/TextUI/Tags/BlockTag.cs
deleted file mode 100644
index ebffbdf..0000000
--- a/src/Text/Def/TextUI/Tags/BlockTag.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-namespace Microsoft.VisualStudio.Text.Tagging
-{
- using Microsoft.VisualStudio.Text.Adornments;
-
- /// <summary>
- /// An implementation of <see cref="IBlockTag" />.
- /// </summary>
- public abstract class BlockTag : IBlockTag
- {
- public BlockTag(SnapshotSpan span, SnapshotSpan statementSpan, IBlockTag parent, string type, bool isCollapsible, bool isDefaultCollapsed, bool isImplementation, object collapsedForm, object collapsedHintForm)
- {
- this.Span = span;
- this.Level = (parent == null) ? 0 : (parent.Level + 1);
- this.StatementSpan = statementSpan;
- this.Parent = parent;
- this.Type = type;
- this.IsCollapsible = isCollapsible;
- this.IsDefaultCollapsed = isDefaultCollapsed;
- this.IsImplementation = isImplementation;
- this.CollapsedForm = collapsedForm;
- this.CollapsedHintForm = collapsedHintForm;
- }
-
- /// <summary>
- /// Gets the span of the structural block.
- /// </summary>
- public virtual SnapshotSpan Span { get; set; }
-
- /// <summary>
- /// Gets the level of nested-ness of the structural block.
- /// </summary>
- public virtual int Level { get; }
-
- /// <summary>
- /// Gets the span of the statement that control the structral block.
- /// </summary>
- /// <remarks>
- /// <para>
- /// For example, in the following snippet of code,
- /// <code>
- /// if (condition1 &amp;&amp;
- /// condition2) // comment
- /// {
- /// something;
- /// }
- /// </code>
- /// this.StatementSpan would extend from the start of the "if" to the end of comment.
- /// this.Span would extend from before the "{" to the end of the "}".
- /// </para>
- /// </remarks>
- public virtual SnapshotSpan StatementSpan { get; set; }
-
- /// <summary>
- /// Gets the hierarchical parent of the structural block.
- /// </summary>
- public virtual IBlockTag Parent { get; }
-
- /// <summary>
- /// Determines the semantic type of the structural block.
- /// <remarks>
- /// See <see cref="PredefinedStructureTypes"/> for the canonical types.
- /// Use <see cref="PredefinedStructureTypes.Nonstructural"/> for blocks that will not have any visible affordance
- /// (but will be used for outlining).
- /// </remarks>
- /// </summary>
- public virtual string Type { get; }
-
- /// <summary>
- /// Determines whether a block can be collapsed.
- /// </summary>
- public virtual bool IsCollapsible { get; }
-
- /// <summary>
- /// Determines whether a block is collapsed by default.
- /// </summary>
- public virtual bool IsDefaultCollapsed { get; }
-
- /// <summary>
- /// Determines whether a block is an block region.
- /// </summary>
- /// <remarks>
- /// Implementation blocks are the blocks of code following a method definition.
- /// They are used for commands such as the Visual Studio Collapse to Definition command,
- /// which hides the implementation block and leaves only the method definition exposed.
- /// </remarks>
- public virtual bool IsImplementation { get; }
-
- /// <summary>
- /// Gets the data object for the collapsed UI. If the default is set, returns null.
- /// </summary>
- public virtual object CollapsedForm { get; }
-
- /// <summary>
- /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null.
- /// </summary>
- public virtual object CollapsedHintForm { get; }
- }
-}
diff --git a/src/Text/Def/TextUI/Tags/IBlockTag.cs b/src/Text/Def/TextUI/Tags/IBlockTag.cs
deleted file mode 100644
index 684db50..0000000
--- a/src/Text/Def/TextUI/Tags/IBlockTag.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-namespace Microsoft.VisualStudio.Text.Tagging
-{
- using Microsoft.VisualStudio.Text.Adornments;
-
- /// <summary>
- /// Represents a structural code block, which is used for vertical structural line adornments.
- /// </summary>
- public interface IBlockTag : ITag
- {
- /// <summary>
- /// Gets the span of the structural block.
- /// </summary>
- SnapshotSpan Span { get; }
-
- /// <summary>
- /// Gets the level of nested-ness of the structural block.
- /// </summary>
- int Level { get; }
-
- /// <summary>
- /// Gets the span of the statement that control the structral block.
- /// </summary>
- /// <remarks>
- /// <para>
- /// For example, in the following snippet of code,
- /// <code>
- /// if (condition1 &amp;&amp;
- /// condition2) // comment
- /// {
- /// something;
- /// }
- /// </code>
- /// this.StatementSpan would extend from the start of the "if" to the end of comment.
- /// this.Span would extend from before the "{" to the end of the "}".
- /// </para>
- /// </remarks>
- SnapshotSpan StatementSpan { get; }
-
- /// <summary>
- /// Gets the hierarchical parent of the structural block.
- /// </summary>
- IBlockTag Parent { get; }
-
- /// <summary>
- /// Determines the semantic type of the structural block.
- /// <remarks>
- /// See <see cref="PredefinedStructureTypes"/> for the canonical types.
- /// Use <see cref="PredefinedStructureTypes.Nonstructural"/> for blocks that will not have any visible affordance
- /// (but will be used for outlining).
- /// </remarks>
- /// </summary>
- string Type { get; }
-
- /// <summary>
- /// Determines whether a block can be collapsed.
- /// </summary>
- bool IsCollapsible { get; }
-
- /// <summary>
- /// Determines whether a block is collapsed by default.
- /// </summary>
- bool IsDefaultCollapsed { get; }
-
- /// <summary>
- /// Determines whether a block is an implementation block.
- /// </summary>
- /// <remarks>
- /// Implementation blocks are the blocks of code following a method definition.
- /// They are used for commands such as the Visual Studio Collapse to Definition command,
- /// which hides the implementation block and leaves only the method definition exposed.
- /// </remarks>
- bool IsImplementation { get; }
-
- /// <summary>
- /// Gets the data object for the collapsed UI. If the default is set, returns null.
- /// </summary>
- object CollapsedForm { get; }
-
- /// <summary>
- /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null.
- /// </summary>
- object CollapsedHintForm { get; }
- }
-}
diff --git a/src/Text/Def/TextUI/Tags/IStructureTag.cs b/src/Text/Def/TextUI/Tags/IStructureTag.cs
new file mode 100644
index 0000000..5a7e950
--- /dev/null
+++ b/src/Text/Def/TextUI/Tags/IStructureTag.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ using Microsoft.VisualStudio.Text.Adornments;
+
+ /// <summary>
+ /// Represents a structural code block, which is used for vertical structural line adornments
+ /// and outlining collapse regions.
+ /// </summary>
+ /// <remarks>
+ /// IStructureTag is the replacement for the <see cref="IBlockTag"/> which should not be used.
+ /// </remarks>
+ public interface IStructureTag : ITag
+ {
+ /// <summary>
+ /// The Snapshot from which this IStructureTag was generated.
+ /// </summary>
+ ITextSnapshot Snapshot { get; }
+
+ /// <summary>
+ /// Gets the span containing the entire contents of the block (minus the block header).
+ /// This span will be collapsed or expanded when the block outlining adornment is invoked.
+ /// </summary>
+ /// <remarks>
+ /// If this parameter is null, block structure adornments will still be drawn as long as
+ /// the <see cref="GuideLineHorizontalAnchorPoint"/> and the <see cref="GuideLineSpan"/> are both provided,
+ /// however, no outlining adornment will be drawn.
+ /// </remarks>
+ Span? OutliningSpan { get; }
+
+ /// <summary>
+ /// Gets the span of the statement that controls the structural block.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// For example, in the following snippet of code,
+ /// <code>
+ /// if (condition1 &amp;&amp;
+ /// condition2) // comment
+ /// {
+ /// something;
+ /// }
+ /// </code>
+ /// this.HeaderSpan would extend from the start of the "if" to the end of comment.
+ /// this.OutliningSpan would extend from the end of "// comment" to the end of the "}".
+ /// </para>
+ /// <para>
+ /// If this parameter is null, block structure adornments will still be drawn as long as
+ /// the <see cref="OutliningSpan"/> is provided, or the <see cref="GuideLineHorizontalAnchorPoint"/>
+ /// and the <see cref="GuideLineSpan"/> are both provided.
+ /// </para>
+ /// </remarks>
+ Span? HeaderSpan { get; }
+
+ /// <summary>
+ /// Gets the vertical span within which the block structure adornment will be drawn.
+ /// </summary>
+ /// <remarks>
+ /// For a block to have an adornment, it must not be of type <see cref="PredefinedStructureTagTypes.Nonstructural"/>,
+ /// and the implementer must also provide the GuideLineHorizontalAnchor. The adornment is drawn from the top of the
+ /// line containing the start of the span to the bottom of the line containing the bottom of the span. If null,
+ /// the GuideLineSpan is inferred from the OutliningSpan and the HeaderSpan.
+ /// </remarks>
+ Span? GuideLineSpan { get; }
+
+ /// <summary>
+ /// Gets the point with which the block structure adornment will be horizontally aligned.
+ /// </summary>
+ /// <remarks>
+ /// This point can be on any line and is used solely for determining horizontal position. If null,
+ /// or if <see cref="GuideLineSpan"/> is null, this point is computed from the HeaderSpan and
+ /// OutliningSpan via heuristics.
+ /// </remarks>
+ int? GuideLineHorizontalAnchorPoint { get; }
+
+ /// <summary>
+ /// Determines the semantic type of the structural block.
+ /// </summary>
+ /// <remarks>
+ /// See <see cref="PredefinedStructureTagTypes"/> for the canonical types.
+ /// Use <see cref="PredefinedStructureTagTypes.Nonstructural"/> for blocks that will not have any visible affordance
+ /// (but will be used for outlining).
+ /// </remarks>
+ string Type { get; }
+
+ /// <summary>
+ /// Determines whether a block can be collapsed.
+ /// </summary>
+ bool IsCollapsible { get; }
+
+ /// <summary>
+ /// Determines whether a block is collapsed by default.
+ /// </summary>
+ bool IsDefaultCollapsed { get; }
+
+ /// <summary>
+ /// Determines whether a block is an implementation block.
+ /// </summary>
+ /// <remarks>
+ /// Implementation blocks are the blocks of code following a method definition.
+ /// They are used for commands such as the Visual Studio Collapse to Definition command,
+ /// which hides the implementation block and leaves only the method definition exposed.
+ /// </remarks>
+ bool IsImplementation { get; }
+
+ /// <summary>
+ /// Gets the data object for the collapsed UI. If the default is set, returns null.
+ /// </summary>
+ object GetCollapsedForm();
+
+ /// <summary>
+ /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null.
+ /// </summary>
+ object GetCollapsedHintForm();
+ }
+}
diff --git a/src/Text/Def/TextUI/Tags/StructureTag.cs b/src/Text/Def/TextUI/Tags/StructureTag.cs
new file mode 100644
index 0000000..1cea08c
--- /dev/null
+++ b/src/Text/Def/TextUI/Tags/StructureTag.cs
@@ -0,0 +1,207 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ using System;
+ using Microsoft.VisualStudio.Text.Adornments;
+ using Microsoft.VisualStudio.Text.UI.Adornments;
+
+ /// <summary>
+ /// An implementation of <see cref="IStructureTag" />.
+ /// </summary>
+ /// <remarks>
+ /// Using this class is the recommended way to create an instance of <see cref="IStructureElement"/>
+ /// for most purposes. IStructureTag is the replacement for the IBlockTag which should not be used.
+ /// </remarks>
+ public class StructureTag : IStructureTag
+ {
+ private readonly object collapsedForm;
+ private readonly object collapsedHintForm;
+
+ /// <summary>
+ /// Constructs an instance of the <see cref="IStructureTag"/>.
+ /// </summary>
+ /// <remarks>
+ /// StructureTag is intended to replace <see cref="IBlockTag"/> and offers more explicit control
+ /// of the block structure adornments. This class operates on the pay-to-play principle, in that,
+ /// it will allow you to create a tag with just a subset of fields, but if a field is missing, it
+ /// will attempt to guess the missing fields from the information that it has. The most useful example
+ /// of this is to omit the GuideLineSpan and GuideLineHorizontalAnchor point to have the API guess
+ /// them from the HeaderSpan and StatementSpan indentation. If enough information is missing, the tag
+ /// does nothing.
+ /// </remarks>
+ /// <param name="snapshot">The snapshot used to generate this StructureTag.</param>
+ /// <param name="outliningSpan">The block contents, used to determine the collapse region.</param>
+ /// <param name="headerSpan">The control statement at the start of the block.</param>
+ /// <param name="guideLineSpan">
+ /// The vertical span within which the block structure guide is drawn.
+ /// If this member is omitted, it is computed from the HeaderSpan and the OutliningSpan via heuristics.</param>
+ /// <param name="guideLineHorizontalAnchor">
+ /// A point capturing the horizontal offset at which the guide is drawn.
+ /// If this member is omitted, it is computed from the HeaderSpan and the OutliningSpan via heuristics.</param>
+ /// <param name="type">The structure type of the block.</param>
+ /// <param name="isCollapsible">If true, block will have block adornments.</param>
+ /// <param name="isDefaultCollapsed">If true, block is collapsed by default.</param>
+ /// <param name="isImplementation">Defines whether or not the block defines a region following a function declaration.</param>
+ /// <param name="collapsedForm">The form the block appears when collapsed.</param>
+ /// <param name="collapsedHintForm">The form of the collapsed region tooltip.</param>
+ public StructureTag(
+ ITextSnapshot snapshot,
+ Span? outliningSpan = null,
+ Span? headerSpan = null,
+ Span? guideLineSpan = null,
+ int? guideLineHorizontalAnchor = null,
+ string type = null,
+ bool isCollapsible = false,
+ bool isDefaultCollapsed = false,
+ bool isImplementation = false,
+ object collapsedForm = null,
+ object collapsedHintForm = null)
+ {
+ if (snapshot == null)
+ {
+ throw new ArgumentNullException(nameof(snapshot));
+ }
+
+ if (outliningSpan != null && outliningSpan.Value.End > snapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(outliningSpan));
+ }
+
+ if (headerSpan != null && headerSpan.Value.End > snapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(headerSpan));
+ }
+
+ if (guideLineSpan != null && guideLineSpan.Value.End > snapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(guideLineSpan));
+ }
+
+ if (guideLineHorizontalAnchor != null && guideLineHorizontalAnchor.Value > snapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(guideLineHorizontalAnchor));
+ }
+
+ this.Snapshot = snapshot;
+ this.OutliningSpan = outliningSpan;
+ this.HeaderSpan = headerSpan;
+ this.GuideLineSpan = guideLineSpan;
+ this.GuideLineHorizontalAnchorPoint = guideLineHorizontalAnchor;
+ this.Type = type;
+ this.IsCollapsible = isCollapsible;
+ this.IsDefaultCollapsed = isDefaultCollapsed;
+ this.IsImplementation = isImplementation;
+ this.collapsedForm = collapsedForm;
+ this.collapsedHintForm = collapsedHintForm;
+ }
+
+ /// <summary>
+ /// The Snapshot from which this structure tag was generated.
+ /// </summary>
+ public virtual ITextSnapshot Snapshot { get; }
+
+ /// <summary>
+ /// Gets the span containing the entire contents of the block (minus the block header).
+ /// This span will be collapsed or expanded when the block outlining adornment is invoked.
+ /// </summary>
+ /// <remarks>
+ /// If this parameter is null, block structure adornments will still be drawn as long as
+ /// the <see cref="GuideLineHorizontalAnchorPoint"/> and the <see cref="GuideLineSpan"/> are both provided,
+ /// however, no outlining adornment will be drawn.
+ /// </remarks>
+ public virtual Span? OutliningSpan { get; set; }
+
+ /// <summary>
+ /// Gets the span of the statement that controls the structural block.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// For example, in the following snippet of code,
+ /// <code>
+ /// if (condition1 &amp;&amp;
+ /// condition2) // comment
+ /// {
+ /// something;
+ /// }
+ /// </code>
+ /// this.HeaderSpan would extend from the start of the "if" to the end of comment.
+ /// this.OutliningSpan would extend from the end of "// comment" to the end of the "}".
+ /// </para>
+ /// <para>
+ /// If this parameter is null, block structure adornments will still be drawn as long as
+ /// the <see cref="OutliningSpan"/> is provided, or the <see cref="GuideLineHorizontalAnchorPoint"/>
+ /// and the <see cref="GuideLineSpan"/> are both provided.
+ /// </para>
+ /// </remarks>
+ public virtual Span? HeaderSpan { get; }
+
+ /// <summary>
+ /// Gets the point with which the block structure adornment will be horizontally aligned.
+ /// </summary>
+ /// <remarks>
+ /// This point can be on any line and is used solely for determining horizontal position. If null,
+ /// this point is computed from the HeaderSpan and OutliningSpan via heuristics.
+ /// </remarks>
+ public virtual int? GuideLineHorizontalAnchorPoint { get; }
+
+ /// <summary>
+ /// Gets the vertical span within which the block structure adornment will be drawn.
+ /// </summary>
+ /// <remarks>
+ /// For a block to have an adornment, it must not be of type <see cref="PredefinedStructureTagTypes.Nonstructural"/>,
+ /// and the implementer must also provide the GuideLineHorizontalAnchor. The adornment is drawn from the top of the
+ /// line containing the start of the span to the bottom of the line containing the bottom of the span. If null,
+ /// the GuideLineSpan is inferred from the OutliningSpan and the HeaderSpan.
+ /// </remarks>
+ public virtual Span? GuideLineSpan { get; }
+
+ /// <summary>
+ /// Determines the semantic type of the structural block.
+ /// </summary>
+ /// <remarks>
+ /// See <see cref="PredefinedStructureTagTypes"/> for the canonical types.
+ /// Use <see cref="PredefinedStructureTagTypes.Nonstructural"/> for blocks that will not have any visible affordance
+ /// (but will be used for outlining).
+ /// </remarks>
+ public virtual string Type { get; }
+
+ /// <summary>
+ /// Determines whether or not a block can be collapsed.
+ /// </summary>
+ public virtual bool IsCollapsible { get; }
+
+ /// <summary>
+ /// Determines whether a block is collapsed by default.
+ /// </summary>
+ public virtual bool IsDefaultCollapsed { get; }
+
+ /// <summary>
+ /// Determines whether a StructureTag represents an implementation block region.
+ /// </summary>
+ /// <remarks>
+ /// Implementation blocks are the blocks of code following a method definition.
+ /// They are used for commands such as the Visual Studio Collapse to Definition command,
+ /// which hides the implementation block and leaves only the method definition exposed.
+ /// </remarks>
+ public virtual bool IsImplementation { get; }
+
+ /// <summary>
+ /// Gets the data object for the collapsed UI. If the default is set, returns null.
+ /// </summary>
+ public virtual object GetCollapsedForm()
+ {
+ return this.collapsedForm;
+ }
+
+ /// <summary>
+ /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null.
+ /// </summary>
+ public virtual object GetCollapsedHintForm()
+ {
+ return this.collapsedHintForm;
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs
new file mode 100644
index 0000000..7ccd6cb
--- /dev/null
+++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs
@@ -0,0 +1,370 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Threading;
+
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ /// <summary>
+ /// Defines the aggregator for all the classifiers - and is available to the external world via a VisualStudio
+ /// Service
+ /// </summary>
+ internal sealed class ClassifierAggregator : IAccurateClassifier, IDisposable
+ {
+ #region Private Members
+
+ private ITextBuffer _textBuffer;
+ private IClassificationTypeRegistryService _classificationTypeRegistry;
+ private IAccurateTagAggregator<IClassificationTag> _tagAggregator;
+ #endregion // Private Members
+
+ #region Constructors
+
+ internal ClassifierAggregator(ITextBuffer textBuffer,
+ IBufferTagAggregatorFactoryService bufferTagAggregatorFactory,
+ IClassificationTypeRegistryService classificationTypeRegistry)
+ {
+ // Validate.
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ if (bufferTagAggregatorFactory == null)
+ {
+ throw new ArgumentNullException("bufferTagAggregatorFactory");
+ }
+ if (classificationTypeRegistry == null)
+ {
+ throw new ArgumentNullException("classificationTypeRegistry");
+ }
+
+ _textBuffer = textBuffer;
+ _classificationTypeRegistry = classificationTypeRegistry;
+
+ // Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type.
+ _tagAggregator = bufferTagAggregatorFactory.CreateTagAggregator<IClassificationTag>(textBuffer, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator<IClassificationTag>;
+ _tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged;
+ }
+
+ internal ClassifierAggregator(ITextView textView,
+ IViewTagAggregatorFactoryService viewTagAggregatorFactory,
+ IClassificationTypeRegistryService classificationTypeRegistry)
+ {
+ // Validate.
+ if (textView == null)
+ {
+ throw new ArgumentNullException("textView");
+ }
+ if (viewTagAggregatorFactory == null)
+ {
+ throw new ArgumentNullException("viewTagAggregatorFactory");
+ }
+ if (classificationTypeRegistry == null)
+ {
+ throw new ArgumentNullException("classificationTypeRegistry");
+ }
+
+ _textBuffer = textView.TextBuffer;
+ _classificationTypeRegistry = classificationTypeRegistry;
+
+ // Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type.
+ _tagAggregator = viewTagAggregatorFactory.CreateTagAggregator<IClassificationTag>(textView, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator<IClassificationTag>;
+ _tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged;
+ }
+
+ #endregion // Constructors
+
+ #region Exposed Methods
+
+ /// <summary>
+ /// Gets all classification spans that overlap the given range of text
+ /// </summary>
+ /// <param name="span">
+ /// The span of text of interest
+ /// </param>
+ /// <returns>
+ /// A list of lists of ClassificationSpans that intersect with the given range
+ /// </returns>
+ public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
+ {
+ return this.InternalGetClassificationSpans(span, _tagAggregator.GetTags(span));
+ }
+
+ public IList<ClassificationSpan> GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel)
+ {
+ return this.InternalGetClassificationSpans(span, _tagAggregator.GetAllTags(span, cancel));
+ }
+ #endregion // Exposed Methods
+
+ #region IDisposable
+ public void Dispose()
+ {
+ if (_tagAggregator != null)
+ {
+ _tagAggregator.BatchedTagsChanged -= OnBatchedTagsChanged;
+ _tagAggregator.Dispose();
+
+ _tagAggregator = null;
+ }
+ }
+ #endregion
+
+ #region Public Events
+
+ /// <summary>
+ /// Notifies a classification change. The aggregator also bubbles ClassificationChanged
+ /// events up from aggregated classifiers
+ /// </summary>
+ public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
+
+ #endregion // Public Events
+
+ #region Event Handlers
+
+ private void OnBatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e)
+ {
+ var tempEvent = ClassificationChanged;
+ if (tempEvent != null)
+ {
+ foreach (var mappingSpan in e.Spans)
+ {
+ foreach (var span in mappingSpan.GetSpans(_textBuffer))
+ {
+ tempEvent(this, new ClassificationChangedEventArgs(span));
+ }
+ }
+ }
+ }
+
+ #endregion // Event Handlers
+
+ #region Private Helpers
+
+ private IList<ClassificationSpan> InternalGetClassificationSpans(SnapshotSpan span, IEnumerable<IMappingTagSpan<IClassificationTag>> tags)
+ {
+ List<ClassificationSpan> allSpans = new List<ClassificationSpan>();
+ foreach (var tagSpan in tags)
+ {
+ NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(span.Snapshot.TextBuffer);
+ for (int s = 0; s < tagSpans.Count; ++s)
+ {
+ allSpans.Add(new ClassificationSpan(
+ tagSpans[s].TranslateTo(span.Snapshot, SpanTrackingMode.EdgeExclusive),
+ tagSpan.Tag.ClassificationType));
+ }
+ }
+
+ return this.NormalizeClassificationSpans(span, allSpans);
+ }
+
+ #region Private classes
+ struct PointData
+ {
+ public readonly bool IsStart;
+ public readonly int Position;
+ public readonly IClassificationType ClassificationType;
+
+ public PointData(bool isStart, int position, IClassificationType classificationType)
+ {
+ this.IsStart = isStart;
+ this.Position = position;
+ this.ClassificationType = classificationType;
+ }
+ }
+
+ class OpenSpanData
+ {
+ public readonly IClassificationType ClassificationType;
+
+ public int Count = 1; // How many open classification spans with this classification type are there?
+
+ public OpenSpanData(IClassificationType classificationType)
+ {
+ this.ClassificationType = classificationType;
+ }
+ };
+ #endregion
+
+ /// <summary>
+ /// Normalizes a list of classifications. Given an arbitrary set of ClassificationSpan lists, creates
+ /// a new list with each ClassificationSpan, sorted by position and with the appropriate transient
+ /// classification types for spans that had multiple classifications.
+ /// </summary>
+ /// <param name="spans">A list of lists that contain ClassificationSpans.</param>
+ /// <param name="requestedRange">
+ /// The requested range of this call so results can be trimmed if there are any
+ /// classifiers that returned spans past the requested range they can be trimmed.
+ /// </param>
+ /// <returns>A sorted list of normalized spans.</returns>
+ /// <remarks>
+ /// Basic algorithm:
+ /// Create a list of the starting and ending points for each classification (clipped against requestedRange).
+ /// Sort the points list by position. If the positions are the same, starting points go before ending points (this
+ /// prevents a seam between two adjoining classifications of the same type).
+ ///
+ /// Make a list of the currently open spans.
+ /// Walk the points in order.
+ /// When encountering a starting point:
+ /// If there is already an open span of starting point's type, increment the count associated with the open span.
+ /// Otherwise, add a new classification span based on the currently open spans and then add an open span (based on the startpoint) to the list of open spans.
+ ///
+ /// When encountering an ending point:
+ /// Check the count for the open span of the ending point's type (there must be one):
+ /// If it is one, add a new classiciation based on the open spans and remove that open span from the list of open spans.
+ /// Otherwise, decrement the count associated with the open span.
+ /// </remarks>
+ private List<ClassificationSpan> NormalizeClassificationSpans(SnapshotSpan requestedRange, IList<ClassificationSpan> spans)
+ {
+ // Add the start and end points of each classification to one sorted list
+ List<PointData> points = new List<PointData>(spans.Count * 2);
+ int lastEndPoint = -1;
+ IClassificationType lastClassificationType = null;
+ bool requiresNormalization = false;
+ foreach (ClassificationSpan classification in spans)
+ {
+ // Only consider classifications that overlap the requested span
+ Span? span = requestedRange.Overlap(classification.Span.TranslateTo(requestedRange.Snapshot, SpanTrackingMode.EdgeExclusive));
+
+ if (span.HasValue)
+ {
+ // Use a <= test so that adjoining classifications will go through the normalizing process
+ // (so that adjoining classifications of the same type will be merged into a single
+ // classification span).
+ Span overlap = span.Value;
+
+ if ((overlap.Start < lastEndPoint) ||
+ ((overlap.Start == lastEndPoint) && (classification.ClassificationType == lastClassificationType)))
+ {
+ requiresNormalization = true;
+ }
+
+ lastEndPoint = overlap.End;
+ lastClassificationType = classification.ClassificationType;
+
+ points.Add(new PointData(true, overlap.Start, lastClassificationType));
+ points.Add(new PointData(false, overlap.End, lastClassificationType));
+ }
+ }
+
+ List<ClassificationSpan> results = new List<ClassificationSpan>();
+ if (!requiresNormalization)
+ {
+ // The generated points list is already sorted, so simple generate a list of classifications from it without normalizing
+ // (we can't use spans since its classifications have not been clipped to requestedRange).
+ for (int i = 1; (i < points.Count); i += 2)
+ {
+ PointData startPoint = points[i - 1];
+ PointData endPoint = points[i];
+
+ results.Add(new ClassificationSpan(new SnapshotSpan(requestedRange.Snapshot, startPoint.Position, endPoint.Position - startPoint.Position),
+ startPoint.ClassificationType));
+ }
+ }
+ else
+ {
+ points.Sort(Compare);
+
+ int nextSpanStart = 0;
+
+ List<OpenSpanData> openSpans = new List<OpenSpanData>();
+ for (int p = 0; p < points.Count; ++p)
+ {
+ PointData point = points[p];
+ OpenSpanData openSpanData = null;
+ // write this little search explicitly instead of using the Find method, because allocating
+ // a delegate here in a high-frequency loop allocates noticeable amounts of memory
+ for (int os = 0; os < openSpans.Count; ++os)
+ {
+ if (openSpans[os].ClassificationType == point.ClassificationType)
+ {
+ openSpanData = openSpans[os];
+ break;
+ }
+ }
+ if (point.IsStart)
+ {
+ if (openSpanData != null)
+ {
+ ++(openSpanData.Count);
+ }
+ else
+ {
+ if ((openSpans.Count > 0) && (point.Position > nextSpanStart))
+ {
+ this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results);
+ }
+ nextSpanStart = point.Position;
+ openSpans.Add(new OpenSpanData(point.ClassificationType));
+ }
+ }
+ else
+ {
+ if (openSpanData.Count > 1)
+ {
+ --(openSpanData.Count);
+ }
+ else
+ {
+ if (point.Position > nextSpanStart)
+ {
+ this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results);
+ }
+ nextSpanStart = point.Position;
+ openSpans.Remove(openSpanData);
+ }
+ }
+ }
+ }
+
+ // Return our list of aggregated spans.
+ return results;
+ }
+
+ private int Compare(PointData a, PointData b)
+ {
+ if (a.Position == b.Position)
+ return (b.IsStart.CompareTo(a.IsStart)); // startpoints go before end points when positions are tied
+ else
+ return (a.Position - b.Position);
+ }
+
+ /// <summary>
+ /// Add a classification span to <paramref name="results"/> that corresponds to the open classification spans in <paramref name="openSpans"/>
+ /// between <paramref name="start"/> and <paramref name="end"/>.
+ /// </summary>
+ private void AddClassificationSpan(List<OpenSpanData> openSpans, ITextSnapshot snapshot, int start, int end, IList<ClassificationSpan> results)
+ {
+ IClassificationType classificationType;
+
+ if (openSpans.Count == 1)
+ {
+ // If there is only one classification type, then don't create a transient type.
+ classificationType = openSpans[0].ClassificationType;
+ }
+ else
+ {
+ // There is more than one classification type, create a transient type that corresponds
+ // to all the open classificaiton types.
+ List<IClassificationType> classifications = new List<IClassificationType>(openSpans.Count);
+ foreach (OpenSpanData osd in openSpans)
+ classifications.Add(osd.ClassificationType);
+
+ classificationType = _classificationTypeRegistry.CreateTransientClassificationType(classifications);
+ }
+
+ results.Add(new ClassificationSpan(new SnapshotSpan(snapshot, start, end - start),
+ classificationType));
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs
new file mode 100644
index 0000000..bad3402
--- /dev/null
+++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs
@@ -0,0 +1,42 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ /// <summary>
+ /// Exports the <see cref="IClassifierAggregatorService"/> component.
+ /// </summary>
+ [Export(typeof(IClassifierAggregatorService))]
+ [Export(typeof(IViewClassifierAggregatorService))]
+ internal sealed class ClassifierAggregatorService : IClassifierAggregatorService, IViewClassifierAggregatorService
+ {
+ [Import]
+ internal IBufferTagAggregatorFactoryService _bufferTagAggregatorFactory { get; set; }
+
+ [Import]
+ internal IViewTagAggregatorFactoryService _viewTagAggregatorFactory { get; set; }
+
+ [Import]
+ internal IClassificationTypeRegistryService _classificationTypeRegistry { get; set; }
+
+ public IClassifier GetClassifier(ITextBuffer textBuffer)
+ {
+ return new ClassifierAggregator(textBuffer, _bufferTagAggregatorFactory, _classificationTypeRegistry);
+ }
+
+ public IClassifier GetClassifier(ITextView textView)
+ {
+ return new ClassifierAggregator(textView, _viewTagAggregatorFactory, _classificationTypeRegistry);
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs
new file mode 100644
index 0000000..653e768
--- /dev/null
+++ b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs
@@ -0,0 +1,98 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ internal class ClassifierTagger : IAccurateTagger<ClassificationTag>, IDisposable
+ {
+ internal IList<IClassifier> Classifiers { get; private set; }
+
+ internal ClassifierTagger(IList<IClassifier> classifiers)
+ {
+ Classifiers = classifiers;
+
+ foreach(var classifier in classifiers)
+ {
+ classifier.ClassificationChanged += OnClassificationChanged;
+ }
+ }
+
+ #region ITagger<ClassificationTag> members
+ public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
+ {
+ foreach (IClassifier classifier in Classifiers)
+ {
+ foreach(var snapshotSpan in spans)
+ {
+ foreach(var classificationSpan in classifier.GetClassificationSpans(snapshotSpan))
+ {
+ yield return new TagSpan<ClassificationTag>(
+ classificationSpan.Span,
+ new ClassificationTag(classificationSpan.ClassificationType));
+ }
+ }
+ }
+ }
+
+ public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
+ #endregion
+
+ #region IAccurateTagger<ClassificationTag> members
+ public IEnumerable<ITagSpan<ClassificationTag>> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancel)
+ {
+ foreach (IClassifier classifier in Classifiers)
+ {
+ IAccurateClassifier classifier2 = classifier as IAccurateClassifier;
+
+ foreach (var snapshotSpan in spans)
+ {
+ foreach (var classificationSpan in (classifier2 != null)
+ ? classifier2.GetAllClassificationSpans(snapshotSpan, cancel)
+ : classifier.GetClassificationSpans(snapshotSpan))
+ {
+ yield return new TagSpan<ClassificationTag>(
+ classificationSpan.Span,
+ new ClassificationTag(classificationSpan.ClassificationType));
+ }
+ }
+ }
+ }
+ #endregion
+
+ #region IDisposable members
+
+ public void Dispose()
+ {
+ foreach(var classifier in Classifiers)
+ {
+ classifier.ClassificationChanged -= OnClassificationChanged;
+ }
+
+ Classifiers.Clear();
+
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Handles the classification Changed events from the classifiers by turning
+ /// them into TagsChanged events.
+ /// </summary>
+ void OnClassificationChanged(object sender, ClassificationChangedEventArgs e)
+ {
+ var tempEvent = TagsChanged;
+ if (tempEvent != null)
+ tempEvent(this, new SnapshotSpanEventArgs(e.ChangeSpan));
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs b/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs
new file mode 100644
index 0000000..08ebac3
--- /dev/null
+++ b/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs
@@ -0,0 +1,43 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ [Export(typeof(ITaggerProvider))]
+ [ContentType("any")]
+ [TagType(typeof(ClassificationTag))]
+ internal class ClassifierTaggerProvider : ITaggerProvider
+ {
+ [ImportMany(typeof(IClassifierProvider))]
+ internal List<Lazy<IClassifierProvider, INamedContentTypeMetadata>> _classifierProviders { get; set; }
+
+ [Import]
+ internal GuardedOperations _guardedOperations { get; set; }
+
+ [Import]
+ private IContentTypeRegistryService ContentTypeRegistryService { get; set; }
+
+ public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
+ {
+ var classifiers =
+ _guardedOperations.InvokeEligibleFactories
+ (_classifierProviders, (IClassifierProvider provider) => (provider.GetClassifier(buffer)), buffer.ContentType, this.ContentTypeRegistryService, this);
+
+ return classifiers.Count > 0
+ ? new ClassifierTagger(classifiers) as ITagger<T>
+ : null;
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs
new file mode 100644
index 0000000..35a5835
--- /dev/null
+++ b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs
@@ -0,0 +1,126 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ [Export(typeof(ITaggerProvider))]
+ [ContentType("projection")]
+ [TagType(typeof(ClassificationTag))]
+ internal class ProjectionWorkaroundProvider : ITaggerProvider
+ {
+ [Import]
+ internal IDifferenceService diffService { get; set; }
+
+ public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
+ {
+ IProjectionBuffer projectionBuffer = buffer as IProjectionBuffer;
+ if (projectionBuffer == null)
+ return null;
+
+ return new ProjectionWorkaroundTagger(projectionBuffer, diffService) as ITagger<T>;
+ }
+ }
+
+ /// <summary>
+ /// This is a workaround for projection buffers. Currently, the way our projection buffer
+ /// implementation does "minimal" updating is by using *lexical* differencing. The
+ /// problem with this approach is that if you replace a span in a projection buffer
+ /// with a lexically equivalent span from a *different* buffer, the projection buffer
+ /// will event that no changes have been made.
+ /// </summary>
+ internal class ProjectionWorkaroundTagger : ITagger<ClassificationTag>
+ {
+ IProjectionBuffer ProjectionBuffer { get; set; }
+ IDifferenceService diffService;
+
+ internal ProjectionWorkaroundTagger(IProjectionBuffer projectionBuffer, IDifferenceService diffService)
+ {
+ this.ProjectionBuffer = projectionBuffer;
+ this.diffService = diffService;
+ this.ProjectionBuffer.SourceBuffersChanged += SourceSpansChanged;
+ }
+
+ #region ITagger<ClassificationTag> members
+ public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
+ {
+ yield break;
+ }
+
+ public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
+
+ #endregion
+
+ #region Source span differencing + change event
+ private void SourceSpansChanged(object sender, ProjectionSourceSpansChangedEventArgs e)
+ {
+ if (e.Changes.Count == 0)
+ {
+ // If there weren't text changes, but there were span changes, then
+ // send out a classification changed event over the spans that changed.
+ ProjectionSpanDifference difference = ProjectionSpanDiffer.DiffSourceSpans(this.diffService, e.Before, e.After);
+ int pos = 0;
+ int start = int.MaxValue;
+ int end = int.MinValue;
+ foreach (var diff in difference.DifferenceCollection)
+ {
+ pos += GetMatchSize(difference.DeletedSpans, diff.Before);
+ start = Math.Min(start, pos);
+
+ // Now, for every span added in the new snapshot that replaced
+ // the deleted spans, add it to our span to raise changed events
+ // over.
+ for (int i = diff.Right.Start; i < diff.Right.End; i++)
+ {
+ pos += difference.InsertedSpans[i].Length;
+ }
+
+ end = Math.Max(end, pos);
+ }
+
+ if (start != int.MaxValue && end != int.MinValue)
+ {
+ RaiseTagsChangedEvent(new SnapshotSpan(e.After, Span.FromBounds(start, end)));
+ }
+ }
+ }
+
+ private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match)
+ {
+ int size = 0;
+ if (match != null)
+ {
+ Span extent = match.Left;
+ for (int s = extent.Start; s < extent.End; ++s)
+ {
+ size += spans[s].Length;
+ }
+ }
+ return size;
+ }
+
+ private void RaiseTagsChangedEvent(SnapshotSpan span)
+ {
+ var handler = TagsChanged;
+ if (handler != null)
+ {
+ handler(this, new SnapshotSpanEventArgs(span));
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs
new file mode 100644
index 0000000..6f3409f
--- /dev/null
+++ b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs
@@ -0,0 +1,65 @@
+//
+// 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.
+//
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Classification.Implementation
+{
+ internal class ClassificationTypeImpl : IClassificationType
+ {
+ private string name;
+ private FrugalList<IClassificationType> baseTypes;
+
+ internal ClassificationTypeImpl(string name)
+ {
+ this.name = name;
+ }
+
+ internal void AddBaseType(IClassificationType baseType)
+ {
+ if (this.baseTypes == null)
+ {
+ this.baseTypes = new FrugalList<IClassificationType>();
+ }
+
+ this.baseTypes.Add(baseType);
+ }
+
+ public string Classification
+ {
+ get { return this.name; }
+ }
+
+ public bool IsOfType(string type)
+ {
+ if (this.name == type)
+ return true;
+ else if (this.baseTypes != null)
+ {
+ foreach (IClassificationType baseType in this.baseTypes)
+ {
+ if ( baseType.IsOfType(type) )
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public IEnumerable<IClassificationType> BaseTypes
+ {
+ get { return (this.baseTypes != null) ? (IEnumerable<IClassificationType>)(this.baseTypes.AsReadOnly()) : Enumerable.Empty<IClassificationType>(); }
+ }
+
+ public override string ToString()
+ {
+ return this.name;
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs
new file mode 100644
index 0000000..d890eb4
--- /dev/null
+++ b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs
@@ -0,0 +1,253 @@
+//
+// 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.Classification.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Utilities;
+ using System.Collections;
+
+ public interface IClassificationTypeDefinitionMetadata
+ {
+ string Name { get; }
+ [System.ComponentModel.DefaultValue(null)]
+ IEnumerable<string> BaseDefinition { get; }
+ }
+
+ [Export(typeof(IClassificationTypeRegistryService))]
+ internal sealed class ClassificationTypeRegistryService : IClassificationTypeRegistryService
+ {
+ [ImportMany]
+ internal List<Lazy<ClassificationTypeDefinition, IClassificationTypeDefinitionMetadata>> _classificationTypeDefinitions { get; set; }
+
+ [Export]
+ [Name("(TRANSIENT)")]
+ public ClassificationTypeDefinition transientClassificationType;
+
+ [Export]
+ [Name("text")]
+ public ClassificationTypeDefinition textClassificationType;
+
+ #region Private Members
+ private Dictionary<string, ClassificationTypeImpl> _classificationTypes;
+ private Dictionary<string, ClassificationTypeImpl> _transientClassificationTypes;
+
+ #endregion // Private Members
+
+ #region Public Members
+ public IClassificationType GetClassificationType(string type)
+ {
+ ClassificationTypeImpl classificationType = null;
+
+ this.ClassificationTypes.TryGetValue(type, out classificationType);
+
+ return classificationType;
+ }
+
+ /// <summary>
+ /// Create a new classification type and add it to the registry.
+ /// </summary>
+ public IClassificationType CreateClassificationType(string type, IEnumerable<IClassificationType> baseTypes)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (baseTypes == null)
+ {
+ throw new ArgumentNullException("baseTypes");
+ }
+ if (ClassificationTypes.ContainsKey(type))
+ {
+ throw new InvalidOperationException(LookUp.Strings.ClassificationAlreadyAdded);
+ }
+
+ // Use the non-canonical name for the actual type
+ ClassificationTypeImpl classificationType = new ClassificationTypeImpl(type);
+ foreach (var baseType in baseTypes)
+ {
+ classificationType.AddBaseType(baseType);
+ }
+
+ ClassificationTypes.Add(type, classificationType);
+
+ return classificationType;
+ }
+
+ /// <summary>
+ /// Create a transient classification type that can be used to represent
+ /// classification types generated at runtime.
+ /// </summary>
+ /// <param name="baseTypes">The base types associated with this transient type.</param>
+ /// <returns>The new transient type.</returns>
+ public IClassificationType CreateTransientClassificationType(IEnumerable<IClassificationType> baseTypes)
+ {
+ // Validate
+ if (baseTypes == null)
+ {
+ throw new ArgumentNullException("baseTypes");
+ }
+ if (!baseTypes.GetEnumerator().MoveNext())
+ {
+ throw new InvalidOperationException(LookUp.Strings.TransientTypesNeedAtLeastOneBaseType);
+ }
+
+ return BuildTransientClassificationType(baseTypes);
+ }
+
+ /// <summary>
+ /// Create a transient classification type that can be used to represent
+ /// classification types generated at runtime.
+ /// </summary>
+ /// <param name="baseTypes">The base types associated with this transient type.</param>
+ /// <returns>The new transient type.</returns>
+ public IClassificationType CreateTransientClassificationType(params IClassificationType[] baseTypes)
+ {
+ // Validate
+ if (baseTypes == null)
+ {
+ throw new ArgumentNullException("baseTypes");
+ }
+ if (baseTypes.Length == 0)
+ {
+ throw new InvalidOperationException(LookUp.Strings.TransientTypesNeedAtLeastOneBaseType);
+ }
+
+ return BuildTransientClassificationType(baseTypes);
+ }
+ #endregion // Public Members
+
+ #region Private Methods
+
+ /// <summary>
+ /// The transient type contributed by this assembly.
+ /// </summary>
+ private IClassificationType TransientClassificationType
+ {
+ get
+ {
+ return ClassificationTypes["(TRANSIENT)"];
+ }
+ }
+
+ /// <summary>
+ /// The map of classification type names to actual IClassificationTypes.
+ ///
+ /// Used to lazily init the map.
+ /// </summary>
+ private Dictionary<string, ClassificationTypeImpl> ClassificationTypes
+ {
+ get
+ {
+ if (_classificationTypes == null)
+ {
+ _classificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase);
+ BuildClassificationTypes(_classificationTypes);
+ }
+ return _classificationTypes;
+ }
+ }
+
+ /// <summary>
+ /// Consumes all of the IClassificationTypeProvisions in the system to build the
+ /// list of classification types in the system.
+ /// </summary>
+ private void BuildClassificationTypes(Dictionary<string,ClassificationTypeImpl> classificationTypes)
+ {
+ // For each content baseType provision, create an IClassificationType.
+ foreach (Lazy<ClassificationTypeDefinition, IClassificationTypeDefinitionMetadata> classificationTypeDefinition in _classificationTypeDefinitions)
+ {
+ string classificationName = classificationTypeDefinition.Metadata.Name;
+
+ ClassificationTypeImpl type = null;
+
+ if (!classificationTypes.TryGetValue(classificationName, out type))
+ {
+ type = new ClassificationTypeImpl(classificationName);
+ classificationTypes.Add(classificationName, type);
+ }
+
+ IEnumerable<string> baseTypes = classificationTypeDefinition.Metadata.BaseDefinition;
+ if (baseTypes != null)
+ {
+ ClassificationTypeImpl baseType = null;
+
+ foreach (string baseClassificationType in baseTypes)
+ {
+ if (!classificationTypes.TryGetValue(baseClassificationType, out baseType))
+ {
+ baseType = new ClassificationTypeImpl(baseClassificationType);
+ classificationTypes.Add(baseClassificationType, baseType);
+ }
+
+ type.AddBaseType(baseType);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Builds a new transient classification type based on a set of actual base
+ /// types.
+ ///
+ /// With multiple projection buffers, it is possible to have a transient classification
+ /// type with transient types as parents.
+ /// </summary>
+ /// <param name="baseTypes"></param>
+ /// <returns></returns>
+ private IClassificationType BuildTransientClassificationType(IEnumerable<IClassificationType> baseTypes)
+ {
+ // Lazily init
+ if (_transientClassificationTypes == null)
+ {
+ _transientClassificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ List<IClassificationType> sortedBaseTypes = new List<IClassificationType>(baseTypes);
+ sortedBaseTypes.Sort(delegate(IClassificationType a, IClassificationType b)
+ { return string.CompareOrdinal(a.Classification, b.Classification); });
+
+ // Build the transient name
+ StringBuilder sb = new StringBuilder();
+ foreach (IClassificationType type in sortedBaseTypes)
+ {
+ sb.Append(type.Classification);
+ sb.Append(" - ");
+ }
+
+ // Append "(transient)" onto the name.
+ sb.Append(this.TransientClassificationType.Classification);
+
+ // Look for a cached type
+ ClassificationTypeImpl transientType;
+ if (!_transientClassificationTypes.TryGetValue(sb.ToString(), out transientType))
+ {
+ // Didn't find a cached type, so create a new one
+ transientType = new ClassificationTypeImpl(sb.ToString());
+
+ foreach (IClassificationType type in sortedBaseTypes)
+ {
+ transientType.AddBaseType(type);
+ }
+
+ // Add in the transient type as a base type
+ transientType.AddBaseType(TransientClassificationType);
+
+ // Cache this type so it doesn't need to be created again.
+ _transientClassificationTypes[transientType.Classification] = transientType;
+ }
+
+ return transientType;
+ }
+ #endregion // Private Methods
+ }
+}
diff --git a/src/Text/Impl/ClassificationType/Strings.Designer.cs b/src/Text/Impl/ClassificationType/Strings.Designer.cs
new file mode 100644
index 0000000..7ecc53b
--- /dev/null
+++ b/src/Text/Impl/ClassificationType/Strings.Designer.cs
@@ -0,0 +1,81 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:2.0.50727.1434
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Text.Classification.Implementation.LookUp {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Logic.Text.Classification.LookUp.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A classification with the same name has already been added..
+ /// </summary>
+ internal static string ClassificationAlreadyAdded {
+ get {
+ return ResourceManager.GetString("ClassificationAlreadyAdded", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A transient classification type needs at least one base type..
+ /// </summary>
+ internal static string TransientTypesNeedAtLeastOneBaseType {
+ get {
+ return ResourceManager.GetString("TransientTypesNeedAtLeastOneBaseType", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/ClassificationType/Strings.resx b/src/Text/Impl/ClassificationType/Strings.resx
new file mode 100644
index 0000000..5f64ec1
--- /dev/null
+++ b/src/Text/Impl/ClassificationType/Strings.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ClassificationAlreadyAdded" xml:space="preserve">
+ <value>A classification with the same name has already been added.</value>
+ </data>
+ <data name="TransientTypesNeedAtLeastOneBaseType" xml:space="preserve">
+ <value>A transient classification type needs at least one base type.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs
new file mode 100644
index 0000000..a4e0a78
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs
@@ -0,0 +1,136 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.VisualStudio.Text.Differencing;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ /// <summary>
+ /// This is a decomposition of the given string into characters.
+ /// Note that this is still a DecomposedStringList, which is an IList&lt;string&gt;,
+ /// and so each string will be one character in length.
+ /// </summary>
+ internal class CharacterDecompositionList : ITokenizedStringListInternal
+ {
+ string _originalString;
+ SnapshotSpan _originalSpan;
+
+ public CharacterDecompositionList(string original)
+ {
+ _originalString = original;
+ }
+
+ public CharacterDecompositionList(SnapshotSpan original)
+ {
+ _originalSpan = original;
+ }
+
+ public string Original
+ {
+ get
+ {
+ // A call to GetText() here could be very expensive in memory. Be careful!
+ return _originalString ?? _originalSpan.GetText();
+ }
+ }
+
+ public string OriginalSubstring(int start, int length)
+ {
+ if (_originalString != null)
+ return _originalString.Substring(start, length);
+ else
+ return _originalSpan.Snapshot.GetText(start + _originalSpan.Start.Position, length);
+ }
+
+ public Span GetElementInOriginal(int index)
+ {
+ //If index == count, return a zero-length span at the end.
+ return new Span(index, (index < this.Count) ? 1 : 0);
+ }
+
+ public Span GetSpanInOriginal(Span span)
+ {
+ return span;
+ }
+
+ public int IndexOf(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Insert(int index, string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ throw new NotSupportedException();
+ }
+
+ public string this[int index]
+ {
+ get
+ {
+ return this.OriginalSubstring(index, 1);
+ }
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public void Add(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ public bool Contains(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void CopyTo(string[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ public int Count
+ {
+ get { return _originalString != null ? _originalString.Length : _originalSpan.Length; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public bool Remove(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public IEnumerator<string> GetEnumerator()
+ {
+ for (int i = 0; i < Count; i++)
+ yield return this[i];
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs
new file mode 100644
index 0000000..85f1156
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs
@@ -0,0 +1,153 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+// Ignore deprecated (IHierarchicalStringDifferenceService is deprecated)
+#pragma warning disable 0618
+
+ [Export(typeof(IHierarchicalStringDifferenceService))]
+ internal sealed class DefaultTextDifferencingService : ITextDifferencingService, IHierarchicalStringDifferenceService
+ {
+ #region ITextDifferencingService-specific
+
+ public IHierarchicalDifferenceCollection DiffSnapshotSpans(SnapshotSpan leftSpan, SnapshotSpan rightSpan, StringDifferenceOptions differenceOptions)
+ {
+ return this.DiffSnapshotSpans(leftSpan, rightSpan, differenceOptions, DefaultGetLineTextCallback);
+ }
+
+ public IHierarchicalDifferenceCollection DiffSnapshotSpans(SnapshotSpan leftSpan, SnapshotSpan rightSpan, StringDifferenceOptions differenceOptions, Func<ITextSnapshotLine, string> getLineTextCallback)
+ {
+ StringDifferenceTypes type;
+ ITokenizedStringListInternal left;
+ ITokenizedStringListInternal right;
+ if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Line))
+ {
+ type = StringDifferenceTypes.Line;
+
+ left = new SnapshotLineList(leftSpan, getLineTextCallback, differenceOptions);
+ right = new SnapshotLineList(rightSpan, getLineTextCallback, differenceOptions);
+ }
+ else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Word))
+ {
+ type = StringDifferenceTypes.Word;
+
+ left = new WordDecompositionList(leftSpan, differenceOptions);
+ right = new WordDecompositionList(rightSpan, differenceOptions);
+ }
+ else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Character))
+ {
+ type = StringDifferenceTypes.Character;
+
+ left = new CharacterDecompositionList(leftSpan);
+ right = new CharacterDecompositionList(rightSpan);
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException("differenceOptions");
+ }
+
+ return DiffText(left, right, type, differenceOptions);
+ }
+
+ public IHierarchicalDifferenceCollection DiffStrings(string leftString, string rightString, StringDifferenceOptions differenceOptions)
+ {
+ if (leftString == null)
+ throw new ArgumentNullException(nameof(leftString));
+ if (rightString == null)
+ throw new ArgumentNullException(nameof(rightString));
+
+ StringDifferenceTypes type;
+ ITokenizedStringListInternal left;
+ ITokenizedStringListInternal right;
+ if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Line))
+ {
+ type = StringDifferenceTypes.Line;
+
+ left = new LineDecompositionList(leftString, differenceOptions.IgnoreTrimWhiteSpace);
+ right = new LineDecompositionList(rightString, differenceOptions.IgnoreTrimWhiteSpace);
+ }
+ else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Word))
+ {
+ type = StringDifferenceTypes.Word;
+
+ left = new WordDecompositionList(leftString, differenceOptions);
+ right = new WordDecompositionList(rightString, differenceOptions);
+ }
+ else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Character))
+ {
+ type = StringDifferenceTypes.Character;
+
+ left = new CharacterDecompositionList(leftString);
+ right = new CharacterDecompositionList(rightString);
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException("differenceOptions");
+ }
+
+ return DiffText(left, right, type, differenceOptions);
+ }
+
+ IHierarchicalDifferenceCollection DiffText(ITokenizedStringListInternal left, ITokenizedStringListInternal right, StringDifferenceTypes type, StringDifferenceOptions differenceOptions)
+ {
+ StringDifferenceOptions nextOptions = new StringDifferenceOptions(differenceOptions);
+ nextOptions.DifferenceType &= ~type;
+
+ var diffCollection = ComputeMatches(type, differenceOptions, left, right);
+ return new HierarchicalDifferenceCollection(diffCollection, left, right, this, nextOptions);
+ }
+
+ internal static List<Span> GetContiguousSpans(Span span, ITokenizedStringListInternal tokens)
+ {
+ List<Span> result = new List<Span>();
+ int start = span.Start;
+ for (int i = span.Start + 1; (i < span.End); ++i)
+ {
+ if (tokens.GetElementInOriginal(i - 1).End != tokens.GetElementInOriginal(i).Start)
+ {
+ result.Add(Span.FromBounds(start, i));
+ start = i;
+ }
+ }
+
+ if (start < span.End)
+ {
+ result.Add(Span.FromBounds(start, span.End));
+ }
+
+ return result;
+ }
+
+ internal static string DefaultGetLineTextCallback(ITextSnapshotLine line)
+ {
+ return line.GetTextIncludingLineBreak();
+ }
+
+ static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions,
+ IList<string> leftSequence, IList<string> rightSequence)
+ {
+ return ComputeMatches(differenceType, differenceOptions, leftSequence, rightSequence, leftSequence, rightSequence);
+ }
+
+ static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions,
+ IList<string> leftSequence, IList<string> rightSequence,
+ IList<string> originalLeftSequence, IList<string> originalRightSequence)
+ {
+ return MaximalSubsequenceAlgorithm.DifferenceSequences(leftSequence, rightSequence, originalLeftSequence, originalRightSequence, differenceOptions.ContinueProcessingPredicate);
+ }
+
+ #endregion
+ }
+
+#pragma warning restore 0618
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs b/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs
new file mode 100644
index 0000000..1380f9f
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs
@@ -0,0 +1,54 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Microsoft.VisualStudio.Text.Differencing;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ internal static class DiffChangeCollectionHelper<T>
+ {
+ public static DifferenceCollection<T> Create(Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes, IList<T> originalLeft, IList<T> originalRight)
+ {
+ return new DifferenceCollection<T>(CreateDiffs(changes, originalLeft, originalRight), originalLeft, originalRight);
+ }
+
+ static IList<Difference> CreateDiffs(Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes, IList<T> originalLeft, IList<T> originalRight)
+ {
+ IList<Difference> diffs = new List<Difference>(changes.Length);
+
+ if (changes.Length != 0)
+ {
+ //Create the match before (if any)
+ var change = changes[0];
+ Match before = DifferenceCollection<T>.CreateInitialMatch(change.OriginalStart);
+
+ for (int i = 1; (i < changes.Length); ++i)
+ {
+ var nextChange = changes[i];
+
+ DifferenceCollection<T>.AddDifference(change.OriginalStart, change.OriginalEnd, nextChange.OriginalStart,
+ change.ModifiedStart, change.ModifiedEnd, nextChange.ModifiedStart,
+ diffs, ref before);
+
+ change = nextChange;
+ }
+
+ DifferenceCollection<T>.AddDifference(change.OriginalStart, change.OriginalEnd, originalLeft.Count,
+ change.ModifiedStart, change.ModifiedEnd, originalRight.Count,
+ diffs, ref before);
+ }
+
+ return diffs;
+ }
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs
new file mode 100644
index 0000000..33d06cb
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs
@@ -0,0 +1,161 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ /// <summary>
+ /// The default implementation of IDecomposedDifferenceCollection. This maintains
+ /// a given IDifferenceCollection&lt;string&gt; and a mapping of Difference indices to
+ /// contained differences (if there are any).
+ /// </summary>
+ internal class HierarchicalDifferenceCollection : IHierarchicalDifferenceCollection
+ {
+ private readonly ITokenizedStringListInternal left;
+ private readonly ITokenizedStringListInternal right;
+ private readonly IDifferenceCollection<string> differenceCollection;
+ private readonly ITextDifferencingService differenceService;
+ private readonly StringDifferenceOptions options;
+
+ private readonly ConcurrentDictionary<int, IHierarchicalDifferenceCollection> containedDifferences;
+
+ /// <summary>
+ /// Create a new hierarchical difference collection.
+ /// </summary>
+ /// <param name="differenceCollection">The underlying difference collection for this level
+ /// of the hierarchy.</param>
+ /// <param name="differenceService">The difference service to use for doing the next level of
+ /// differencing</param>
+ /// <param name="options">The options to use for the next level of differencing.
+ /// If <see cref="StringDifferenceOptions.DifferenceType" /> is <c>0</c>, then
+ /// no further differencing will take place.</param>
+ public HierarchicalDifferenceCollection(IDifferenceCollection<string> differenceCollection,
+ ITokenizedStringListInternal left,
+ ITokenizedStringListInternal right,
+ ITextDifferencingService differenceService,
+ StringDifferenceOptions options)
+ {
+ if (differenceCollection == null)
+ throw new ArgumentNullException("differenceCollection");
+ if (left == null)
+ throw new ArgumentNullException("left");
+ if (right == null)
+ throw new ArgumentNullException("right");
+ if (!object.ReferenceEquals(left, differenceCollection.LeftSequence))
+ throw new ArgumentException("left must equal differenceCollection.LeftSequence");
+ if (!object.ReferenceEquals(right, differenceCollection.RightSequence))
+ throw new ArgumentException("right must equal differenceCollection.RightSequence");
+
+ this.left = left;
+ this.right = right;
+
+ this.differenceCollection = differenceCollection;
+ this.differenceService = differenceService;
+ this.options = options;
+
+ containedDifferences = new ConcurrentDictionary<int, IHierarchicalDifferenceCollection>();
+ }
+
+ #region IHierarchicalDifferenceCollection Members
+
+ public ITokenizedStringList LeftDecomposition
+ {
+ get { return left; }
+ }
+
+ public ITokenizedStringList RightDecomposition
+ {
+ get { return right; }
+ }
+
+ public IHierarchicalDifferenceCollection GetContainedDifferences(int index)
+ {
+ if (options.DifferenceType == 0)
+ return null;
+
+ return containedDifferences.GetOrAdd(index, CalculateContainedDiff);
+ }
+
+ /// <summary>
+ /// Calculate the contained difference at the given index. Used by the concurrent dictionary's
+ /// GetOrAdd that takes a value factory.
+ /// </summary>
+ private IHierarchicalDifferenceCollection CalculateContainedDiff(int index)
+ {
+ // We need to compute the next level of differences.
+ var diff = this.Differences[index];
+
+ if (diff.DifferenceType == DifferenceType.Change)
+ {
+ Span leftSpan = this.left.GetSpanInOriginal(diff.Left);
+ Span rightSpan = this.right.GetSpanInOriginal(diff.Right);
+
+ string leftString = this.left.OriginalSubstring(leftSpan.Start, leftSpan.Length);
+ string rightString = this.right.OriginalSubstring(rightSpan.Start, rightSpan.Length);
+
+ return differenceService.DiffStrings(leftString, rightString, options);
+ }
+
+ return null;
+ }
+
+ public bool HasContainedDifferences(int index)
+ {
+ return GetContainedDifferences(index) != null;
+ }
+
+ #endregion
+
+ #region IDifferenceCollection<string> Members
+
+ public IList<Difference> Differences
+ {
+ get { return differenceCollection.Differences; }
+ }
+
+ public IList<string> LeftSequence
+ {
+ get { return left; }
+ }
+
+ public IEnumerable<Tuple<int, int>> MatchSequence
+ {
+ get { return differenceCollection.MatchSequence; }
+ }
+
+ public IList<string> RightSequence
+ {
+ get { return right; }
+ }
+
+ #endregion
+
+ #region IEnumerable<Difference> Members
+
+ public IEnumerator<Difference> GetEnumerator()
+ {
+ return differenceCollection.GetEnumerator();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs
new file mode 100644
index 0000000..7c27e7c
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs
@@ -0,0 +1,83 @@
+//
+// 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.
+//
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ // TODO: Replace the logic in this class with the upcoming Line/Word
+ // split utility.
+
+ /// <summary>
+ /// This is a decomposition of the given string into lines.
+ /// </summary>
+ internal sealed class LineDecompositionList : TokenizedStringList
+ {
+ public LineDecompositionList(string original, bool ignoreTrimWhiteSpace)
+ : base(original)
+ {
+ if (original.Length == 0)
+ {
+ base.Tokens.Add(new Span(0, 0));
+ }
+ else
+ {
+ int firstNonWhitespace = -1;
+ int lastNonWhitespace = -1;
+ int start = 0;
+ int i = 0;
+ while (i < original.Length)
+ {
+ int breakLength = TextUtilities.LengthOfLineBreak(original, i, original.Length);
+ if (breakLength > 0)
+ {
+ i += breakLength;
+ if (ignoreTrimWhiteSpace)
+ {
+ base.Tokens.Add((firstNonWhitespace == -1)
+ ? new Span(i - breakLength, 0)
+ : Span.FromBounds(firstNonWhitespace, lastNonWhitespace + 1));
+ }
+ else
+ {
+ base.Tokens.Add(Span.FromBounds(start, i));
+ }
+
+ start = i;
+
+ firstNonWhitespace = -1;
+ lastNonWhitespace = -1;
+ }
+ else
+ {
+ if (!char.IsWhiteSpace(original[i]))
+ {
+ if (firstNonWhitespace == -1)
+ {
+ firstNonWhitespace = i;
+ }
+ lastNonWhitespace = i;
+ }
+
+ ++i;
+ }
+ }
+
+ if (ignoreTrimWhiteSpace)
+ {
+ base.Tokens.Add((firstNonWhitespace == -1)
+ ? new Span(original.Length, 0)
+ : Span.FromBounds(firstNonWhitespace, lastNonWhitespace + 1));
+ }
+ else
+ {
+ base.Tokens.Add(Span.FromBounds(start, original.Length));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs
new file mode 100644
index 0000000..a40aa72
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs
@@ -0,0 +1,74 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ /// <summary>
+ /// Generate a maximal common subsequence (or longest common subsequence) of
+ /// two sequences (ILists).
+ /// </summary>
+ [Export(typeof(IDifferenceService))]
+ internal sealed class MaximalSubsequenceAlgorithm : IDifferenceService
+ {
+ #region IDifferenceService Members
+ static readonly Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] Empty = new Microsoft.TeamFoundation.Diff.Copy.IDiffChange[0];
+
+ public IDifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right)
+ {
+ return DifferenceSequences<T>(left, right, null);
+ }
+
+ public IDifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right, ContinueProcessingPredicate<T> continueProcessingPredicate)
+ {
+ return DifferenceSequences<T>(left, right, left, right, continueProcessingPredicate);
+ }
+
+ #endregion
+
+ internal static DifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right, IList<T> originalLeft, IList<T> originalRight, ContinueProcessingPredicate<T> continueProcessingPredicate)
+ {
+ if (left == null)
+ throw new ArgumentNullException("left");
+ if (right == null)
+ throw new ArgumentNullException("right");
+
+ Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes;
+ if ((left.Count == 0) || (right.Count == 0))
+ {
+ if ((left.Count == 0) && (right.Count == 0))
+ {
+ changes = MaximalSubsequenceAlgorithm.Empty;
+ }
+ else
+ {
+ changes = new Microsoft.TeamFoundation.Diff.Copy.IDiffChange[1];
+ changes[0] = new Microsoft.TeamFoundation.Diff.Copy.DiffChange(0, left.Count,
+ 0, right.Count);
+ }
+ }
+ else
+ changes = ComputeMaximalSubsequence<T>(left, right, continueProcessingPredicate);
+
+ return DiffChangeCollectionHelper<T>.Create(changes, originalLeft, originalRight);
+ }
+
+ private static Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] ComputeMaximalSubsequence<T>(IList<T> left, IList<T> right, ContinueProcessingPredicate<T> continueProcessingPredicate)
+ {
+ var lcs = new Microsoft.TeamFoundation.Diff.Copy.LcsDiff<T>();
+ var diffs = lcs.Diff(left, right, EqualityComparer<T>.Default, (continueProcessingPredicate == null)
+ ? (Microsoft.TeamFoundation.Diff.Copy.ContinueDifferencePredicate<T>)null
+ : (int originalIndex, IList<T> originalSequence, int longestMatchSoFar) => { return continueProcessingPredicate(originalIndex, originalSequence, longestMatchSoFar);});
+
+ return diffs;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs
new file mode 100644
index 0000000..47df212
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs
@@ -0,0 +1,214 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ /// <summary>
+ /// This class translates an <see cref="ITextSnapshot"/> and a set of line transforms
+ /// into a list of lines that is suitable for diffing. The line transforms are evaluated
+ /// as each line is requested, to minimize the extra memory required to translate every
+ /// line of the snapshot.
+ /// </summary>
+ class SnapshotLineList : IList<string>, ITokenizedStringListInternal
+ {
+ SnapshotSpan _snapshotSpan;
+ Span _lineSpan;
+ Func<ITextSnapshotLine, string> _getLineTextCallback;
+ StringDifferenceOptions _options;
+
+ public SnapshotLineList(SnapshotSpan snapshotSpan, Func<ITextSnapshotLine, string> getLineTextCallback, StringDifferenceOptions options)
+ {
+ if (getLineTextCallback == null)
+ throw new ArgumentNullException("getLineTextCallback");
+ if ((options.DifferenceType & StringDifferenceTypes.Line) == 0)
+ throw new InvalidOperationException("This collection can only be used for line differencing");
+
+ _snapshotSpan = snapshotSpan;
+ _getLineTextCallback = getLineTextCallback;
+ _options = options;
+
+ // Figure out the first and last line in the span
+ var startLine = snapshotSpan.Start.GetContainingLine();
+ int start = snapshotSpan.Start.GetContainingLine().LineNumber;
+
+ //Perf hack to avoid calling GetContainingLine() if the lines are the same.
+ SnapshotPoint endPoint = snapshotSpan.End;
+ int end = ((endPoint.Position < startLine.EndIncludingLineBreak) ? start : endPoint.GetContainingLine().LineNumber) + 1;
+
+ _lineSpan = Span.FromBounds(start, end);
+ }
+
+ public int Count
+ {
+ get { return _lineSpan.Length; }
+ }
+
+ public string this[int index]
+ {
+ get
+ {
+ SnapshotSpan lineSpan = GetSpanOfIndex(index);
+ ITextSnapshotLine line = _snapshotSpan.Snapshot.GetLineFromLineNumber(_lineSpan.Start + index);
+
+ bool isPartialLine = lineSpan.Length != line.LengthIncludingLineBreak;
+
+ string text;
+ if (isPartialLine)
+ text = lineSpan.GetText();
+ else
+ text = _getLineTextCallback(line);
+
+ if (_options.IgnoreTrimWhiteSpace)
+ {
+ if (isPartialLine)
+ {
+ // For a partial line, only trim the sides that are included in the line.
+ // This may not be entirely exact (the partial line may still include what would have been
+ // leading whitespace), but we're ok with it for partial lines, which already don't use the
+ // provided _getLineTextCallback.
+ if (lineSpan.Start == line.Start)
+ text = text.TrimStart();
+ if (lineSpan.End == line.EndIncludingLineBreak)
+ text = text.TrimEnd();
+ }
+ else
+ {
+ text = text.Trim();
+ }
+ }
+
+ return text;
+ }
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ SnapshotSpan GetSpanOfIndex(int index)
+ {
+ if (index < 0 || index >= _lineSpan.Length)
+ throw new ArgumentOutOfRangeException("index");
+
+ ITextSnapshotLine line = _snapshotSpan.Snapshot.GetLineFromLineNumber(_lineSpan.Start + index);
+ SnapshotSpan? lineSpan = line.ExtentIncludingLineBreak.Intersection(_snapshotSpan);
+ if (!lineSpan.HasValue)
+ {
+ Debug.Fail("Unexpected - we have a line with no intersection.");
+ return new SnapshotSpan(line.Start, 0);
+ }
+
+ return lineSpan.Value;
+ }
+
+ #region Not supported
+
+ public int IndexOf(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Insert(int index, string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Add(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ public bool Contains(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void CopyTo(string[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public bool Remove(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ public IEnumerator<string> GetEnumerator()
+ {
+ for (int i = 0; i < Count ; i++)
+ {
+ yield return this[i];
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable<string>)this).GetEnumerator();
+ }
+
+ #endregion
+
+ public string Original
+ {
+ get
+ {
+ return _snapshotSpan.GetText();
+ }
+ }
+
+ public string OriginalSubstring(int startIndex, int length)
+ {
+ return _snapshotSpan.Snapshot.GetText(_snapshotSpan.Start + startIndex, length);
+ }
+
+ public Span GetElementInOriginal(int index)
+ {
+ if (index == _lineSpan.Length)
+ {
+ return new Span(_snapshotSpan.End, 0);
+ }
+ else
+ {
+ // Get the span for the index, but make sure to offset by the _snapshotSpan,
+ // so the coordinates are relative to Original (and not the snapshot itself)
+ SnapshotSpan span = GetSpanOfIndex(index);
+ return new Span(span.Start - _snapshotSpan.Start, span.Length);
+ }
+ }
+
+ public Span GetSpanInOriginal(Span span)
+ {
+ int startPoint = GetElementInOriginal(span.Start).Start;
+
+ if (span.IsEmpty)
+ return new Span(startPoint, 0);
+
+ int endPoint = GetElementInOriginal(span.End - 1).End;
+
+ return Span.FromBounds(startPoint, endPoint);
+ }
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs
new file mode 100644
index 0000000..6e2bfd3
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs
@@ -0,0 +1,974 @@
+//
+// 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.
+//
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+//*************************************************************************
+// The code from this point on is a soure-port of the TFS diff algorithm, to be available
+// in cases where we don't have access to the TFS diff assembly (i.e. outside of Visual Studio).
+//*************************************************************************
+
+namespace Microsoft.TeamFoundation.Diff.Copy
+{
+ //*************************************************************************
+ /// <summary>
+ /// A predicate used by Microsoft.TeamFoundation.Diff.DiffFinder
+ /// that allows callers to stop differencing prematurely.
+ /// </summary>
+ /// <param name="originalIndex">The current index in the original sequence being differenced.</param>
+ /// <param name="originalSequence">The original sequence being differenced.</param>
+ /// <param name="longestMatchSoFar">The length of the longest match so far.</param>
+ /// <returns>true if the algorithm should continue processing, false to stop the algorithm.</returns>
+ /// <remarks>
+ /// When false is returned, the algorithm stops searching for matches and uses
+ /// the information it has computed so far to create the Microsoft.TeamFoundation.Diff.IDiffChange[] array
+ /// that will be returned.
+ /// </remarks>
+ //*************************************************************************
+ public delegate bool ContinueDifferencePredicate<T>(int originalIndex,
+ IList<T> originalSequence, int longestMatchSoFar);
+
+ //*************************************************************************
+ /// <summary>
+ /// An enumeration of the possible change types for a difference operation.
+ /// </summary>
+ //*************************************************************************
+ public enum DiffChangeType
+ {
+ //*********************************************************************
+ /// <summary>
+ /// Content was inserted into the modified sequence.
+ /// </summary>
+ //*********************************************************************
+ Insert,
+
+ //*********************************************************************
+ /// <summary>
+ /// Content was deleted from the original sequence.
+ /// </summary>
+ //*********************************************************************
+ Delete,
+
+ //*********************************************************************
+ /// <summary>
+ /// Content from the original sequence has changed in the modified
+ /// sequence.
+ /// </summary>
+ //*********************************************************************
+ Change
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Represents information about a specific difference between two sequences.
+ /// </summary>
+ //*************************************************************************
+ public interface IDiffChange
+ {
+ //*********************************************************************
+ /// <summary>
+ /// The type of difference.
+ /// </summary>
+ //*********************************************************************
+ DiffChangeType ChangeType { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the first element in the original sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ int OriginalStart { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The number of elements from the original sequence which were
+ /// affected (deleted).
+ /// </summary>
+ //*********************************************************************
+ int OriginalLength { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the last element in the original sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ int OriginalEnd { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the first element in the modified sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ int ModifiedStart { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The number of elements from the modified sequence which were
+ /// affected (added).
+ /// </summary>
+ //*********************************************************************
+ int ModifiedLength { get; }
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the last element in the modified sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ int ModifiedEnd { get; }
+
+ /// <summary>
+ /// This methods combines two IDiffChange objects into one
+ /// </summary>
+ /// <param name="diffChange">The diff change to add</param>
+ /// <returns>An IDiffChange that represnets this + diffChange</returns>
+ IDiffChange Add(IDiffChange diffChange);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Represents information about a specific difference between two sequences.
+ /// </summary>
+ //*************************************************************************
+ internal class DiffChange : IDiffChange
+ {
+ //*********************************************************************
+ /// <summary>
+ /// Constructs a new DiffChange with the given sequence information
+ /// and content.
+ /// </summary>
+ /// <param name="originalStart">The start position of the difference
+ /// in the original sequence.</param>
+ /// <param name="originalLength">The number of elements of the difference
+ /// from the original sequence.</param>
+ /// <param name="modifiedStart">The start position of the difference
+ /// in the modified sequence.</param>
+ /// <param name="modifiedLength">The number of elements of the difference
+ /// from the modified sequence.</param>
+ //*********************************************************************
+ internal DiffChange(int originalStart, int originalLength, int modifiedStart, int modifiedLength)
+ {
+ Debug.Assert(originalLength > 0 || modifiedLength > 0, "originalLength and modifiedLength cannot both be <= 0");
+
+ m_originalStart = originalStart;
+ m_originalLength = originalLength;
+ m_modifiedStart = modifiedStart;
+ m_modifiedLength = modifiedLength;
+ UpdateChangeType();
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Determines the change type from the ranges and updates the value.
+ /// </summary>
+ //*********************************************************************
+ private void UpdateChangeType()
+ {
+ // Figure out what change type this is
+ if (m_originalLength > 0)
+ {
+ if (m_modifiedLength > 0)
+ {
+ m_changeType = DiffChangeType.Change;
+ }
+ else
+ {
+ m_changeType = DiffChangeType.Delete;
+ }
+ }
+ else if (m_modifiedLength > 0)
+ {
+ m_changeType = DiffChangeType.Insert;
+ }
+ m_updateChangeType = false;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// The type of difference.
+ /// </summary>
+ //*********************************************************************
+ public DiffChangeType ChangeType
+ {
+ get
+ {
+ if (m_updateChangeType)
+ {
+ UpdateChangeType();
+ }
+
+ return m_changeType;
+ }
+ }
+ private DiffChangeType m_changeType;
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the first element in the original sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ public int OriginalStart
+ {
+ get { return m_originalStart; }
+ set
+ {
+ m_originalStart = value;
+ m_updateChangeType = true;
+ }
+ }
+ private int m_originalStart;
+
+ //*********************************************************************
+ /// <summary>
+ /// The number of elements from the original sequence which were
+ /// affected.
+ /// </summary>
+ //*********************************************************************
+ public int OriginalLength
+ {
+ get { return m_originalLength; }
+ set
+ {
+ m_originalLength = value;
+ m_updateChangeType = true;
+ }
+ }
+ private int m_originalLength;
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the last element in the original sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ public int OriginalEnd
+ {
+ get { return OriginalStart + OriginalLength; }
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the first element in the modified sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ public int ModifiedStart
+ {
+ get { return m_modifiedStart; }
+ set
+ {
+ m_modifiedStart = value;
+ m_updateChangeType = true;
+ }
+ }
+ private int m_modifiedStart;
+
+ //*********************************************************************
+ /// <summary>
+ /// The number of elements from the modified sequence which were
+ /// affected (added).
+ /// </summary>
+ //*********************************************************************
+ public int ModifiedLength
+ {
+ get { return m_modifiedLength; }
+ set
+ {
+ m_modifiedLength = value;
+ m_updateChangeType = true;
+ }
+ }
+ private int m_modifiedLength;
+
+ //*********************************************************************
+ /// <summary>
+ /// The position of the last element in the modified sequence which
+ /// this change affects.
+ /// </summary>
+ //*********************************************************************
+ public int ModifiedEnd
+ {
+ get { return ModifiedStart + ModifiedLength; }
+ }
+
+ /// <summary>
+ /// This methods combines two DiffChange objects into one
+ /// </summary>
+ /// <param name="diffChange">The diff change to add</param>
+ /// <returns>A IDiffChange object that represnets this + diffChange</returns>
+ public IDiffChange Add(IDiffChange diffChange)
+ {
+ //If the diff change is null then just return this
+ if (diffChange == null)
+ {
+ return this;
+ }
+
+ int originalStart = Math.Min(this.OriginalStart, diffChange.OriginalStart);
+ int originalEnd = Math.Max(this.OriginalEnd, diffChange.OriginalEnd);
+
+ int modifiedStart = Math.Min(this.ModifiedStart, diffChange.ModifiedStart);
+ int modifiedEnd = Math.Max(this.ModifiedEnd, diffChange.ModifiedEnd);
+
+ return new DiffChange(originalStart, originalEnd - originalStart, modifiedStart, modifiedEnd - modifiedStart);
+ }
+
+ // Member variables
+ private bool m_updateChangeType;
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// The types of End of Line terminators.
+ /// </summary>
+ //*************************************************************************
+ public enum EndOfLineTerminator
+ {
+ None, // EndOfFile
+ LineFeed, // \n (UNIX)
+ CarriageReturn, // \r (Mac)
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LineFeed")]
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LineFeed")]
+ CarriageReturnLineFeed, // \r\n (DOS)
+ LineSeparator, // \u2028 (Unicode character)
+ ParagraphSeparator, // \u2029 (Unicode character)
+ NextLine // \u0085 (Unicode character)
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Utility class which tokenizes the given stream into string tokens. Each
+ /// token represents a line from the file as delimited by common EOL sequences.
+ /// (\n, \r\n, \r, \u2028, \u2029, \u85)
+ /// </summary>
+ //*************************************************************************
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Tokenizer")]
+ public class DiffLineTokenizer
+ {
+ //*********************************************************************
+ /// <summary>
+ /// Constructs a new DiffLineTokenizer for the given stream and encoding.
+ /// </summary>
+ /// <param name="stream">The stream to tokenize.</param>
+ /// <param name="encoding">The character encoding of the bytes
+ /// in the stream. If null, this will be automatically detected.</param>
+ //*********************************************************************
+ public DiffLineTokenizer(Stream stream, Encoding encoding)
+ {
+ if (encoding == null)
+ {
+ m_streamReader = new StreamReader(stream, true);
+ }
+ else
+ {
+ m_streamReader = new StreamReader(stream, encoding, true);
+ }
+
+ // Bug 70126: Diff calculated code churn incorrectly for file
+ // We don't want to parse a Unicode EOL character if its not Unicode
+ int codePage = m_streamReader.CurrentEncoding.CodePage;
+ if (codePage == Encoding.Unicode.CodePage || codePage == Encoding.BigEndianUnicode.CodePage ||
+ codePage == Encoding.UTF32.CodePage || codePage == Encoding.UTF7.CodePage ||
+ codePage == Encoding.UTF8.CodePage)
+ {
+ m_isUnicodeEncoding = true;
+ }
+
+ // StreamReader has a perf optimization if we ask for at least as many chars
+ // as the internal byte buffer could produce if it were fully filled.
+ // Their internal byte buffer is 1024 bytes.
+ m_bufferSize = m_streamReader.CurrentEncoding.GetMaxCharCount(1024);
+ m_charBuffer = new char[m_bufferSize];
+ m_stringBuilder = new StringBuilder(80);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Fills up the internal buffer with characters from the stream.
+ /// Returns the number of characters in the buffer and resets the
+ /// current buffer position.
+ /// Returns 0 when EOF.
+ /// </summary>
+ //*********************************************************************
+ private int FillBuffer()
+ {
+ m_currBufferPos = 0;
+ m_numCharsInBuffer = m_streamReader.Read(m_charBuffer, 0, m_bufferSize);
+ return m_numCharsInBuffer;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Gets the next line token as a string from the stream.
+ /// </summary>
+ /// <returns>The next token. Returns null when the end of stream has
+ /// been reached.</returns>
+ //*********************************************************************
+ public string NextLineToken(out EndOfLineTerminator endOfLine)
+ {
+ // The structure of this code is borrowed from StreamReader.ReadLine()
+ // Except that it has been extended out to support Unicode EOL characters.
+ m_stringBuilder.Length = 0;
+ endOfLine = EndOfLineTerminator.None;
+
+ if (m_currBufferPos == m_numCharsInBuffer && FillBuffer() == 0)
+ {
+ // If the buffer was empty, and we couldn't refill it, we're done.
+ return null;
+ }
+ do
+ {
+ int index = m_currBufferPos;
+ do
+ {
+ char ch = m_charBuffer[index];
+ if (ch == '\r' || ch == '\n' ||
+ (m_isUnicodeEncoding && (ch == LineSeparator || ch == ParagraphSeparator || ch == NextLine)))
+ {
+ switch ((int)ch)
+ {
+ case '\r':
+ endOfLine = EndOfLineTerminator.CarriageReturn;
+ break;
+ case '\n':
+ endOfLine = EndOfLineTerminator.LineFeed;
+ break;
+ case LineSeparator:
+ endOfLine = EndOfLineTerminator.LineSeparator;
+ break;
+ case ParagraphSeparator:
+ endOfLine = EndOfLineTerminator.ParagraphSeparator;
+ break;
+ case NextLine:
+ endOfLine = EndOfLineTerminator.NextLine;
+ break;
+ }
+
+ String line;
+ if (m_stringBuilder.Length > 0)
+ {
+ m_stringBuilder.Append(m_charBuffer, m_currBufferPos, index - m_currBufferPos);
+ line = m_stringBuilder.ToString();
+ }
+ else
+ {
+ line = new String(m_charBuffer, m_currBufferPos, index - m_currBufferPos);
+ }
+
+ m_currBufferPos = index + 1;
+ if (ch == '\r' && (m_currBufferPos < m_numCharsInBuffer || FillBuffer() > 0))
+ {
+ if (m_charBuffer[m_currBufferPos] == '\n')
+ {
+ endOfLine = EndOfLineTerminator.CarriageReturnLineFeed;
+ m_currBufferPos++;
+ }
+ }
+
+ return line;
+ }
+ index++;
+ } while (index < m_numCharsInBuffer);
+ m_stringBuilder.Append(m_charBuffer, m_currBufferPos, m_numCharsInBuffer - m_currBufferPos);
+ } while (FillBuffer() > 0);
+
+ return m_stringBuilder.ToString();
+ }
+
+ // Misc Unicode EOL characters.
+ private const int LineSeparator = 0x2028;
+ private const int ParagraphSeparator = 0x2029;
+ private const int NextLine = 0x85;
+
+ // Member variables
+ private bool m_isUnicodeEncoding;
+ private int m_bufferSize;
+ private int m_numCharsInBuffer;
+ private int m_currBufferPos;
+ private char[] m_charBuffer;
+ private StringBuilder m_stringBuilder;
+ private StreamReader m_streamReader;
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// A utility class which helps to create the set of DiffChanges from
+ /// a difference operation. This class accepts original DiffElements and
+ /// modified DiffElements that are involved in a particular change. The
+ /// MarktNextChange() method can be called to mark the seration between
+ /// distinct changes. At the end, the Changes property can be called to retrieve
+ /// the constructed changes.
+ /// </summary>
+ //*************************************************************************
+ internal class DiffChangeHelper : IDisposable
+ {
+ //*********************************************************************
+ /// <summary>
+ /// Constructs a new DiffChangeHelper for the given DiffSequences.
+ /// </summary>
+ /// <param name="originalSequence">The original sequence.</param>
+ /// <param name="modifiedSequnece">The modified sequence.</param>
+ //*********************************************************************
+ public DiffChangeHelper()
+ {
+ m_changes = new List<IDiffChange>();
+ m_originalStart = Int32.MaxValue;
+ m_modifiedStart = Int32.MaxValue;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Disposes of the resources used by this helper class.
+ /// </summary>
+ //*********************************************************************
+ public void Dispose()
+ {
+ if (m_changes != null)
+ {
+ m_changes = null;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Marks the beginning of the next change in the set of differences.
+ /// </summary>
+ //*********************************************************************
+ public void MarkNextChange()
+ {
+ // Only add to the list if there is something to add
+ if (m_originalCount > 0 || m_modifiedCount > 0)
+ {
+ // Add the new change to our list
+ m_changes.Add(new DiffChange(m_originalStart, m_originalCount,
+ m_modifiedStart, m_modifiedCount));
+ }
+
+ // Reset for the next change
+ m_originalCount = 0;
+ m_modifiedCount = 0;
+ m_originalStart = Int32.MaxValue;
+ m_modifiedStart = Int32.MaxValue;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Adds the original element at the given position to the elements
+ /// affected by the current change. The modified index gives context
+ /// to the change position with respect to the original sequence.
+ /// </summary>
+ /// <param name="originalIndex">The index of the original element to add.</param>
+ /// <param name="modifiedIndex">The index of the modified element that
+ /// provides corresponding position in the modified sequence.</param>
+ //*********************************************************************
+ public void AddOriginalElement(int originalIndex, int modifiedIndex)
+ {
+ // The 'true' start index is the smallest of the ones we've seen
+ m_originalStart = Math.Min(m_originalStart, originalIndex);
+ m_modifiedStart = Math.Min(m_modifiedStart, modifiedIndex);
+
+ m_originalCount++;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Adds the modified element at the given position to the elements
+ /// affected by the current change. The original index gives context
+ /// to the change position with respect to the modified sequence.
+ /// </summary>
+ /// <param name="originalIndex">The index of the original element that
+ /// provides corresponding position in the original sequence.</param>
+ /// <param name="modifiedIndex">The index of the modified element to add.</param>
+ //*********************************************************************
+ public void AddModifiedElement(int originalIndex, int modifiedIndex)
+ {
+ // The 'true' start index is the smallest of the ones we've seen
+ m_originalStart = Math.Min(m_originalStart, originalIndex);
+ m_modifiedStart = Math.Min(m_modifiedStart, modifiedIndex);
+
+ m_modifiedCount++;
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Retrieves all of the changes marked by the class.
+ /// </summary>
+ //*********************************************************************
+ public IDiffChange[] Changes
+ {
+ get
+ {
+ if (m_originalCount > 0 || m_modifiedCount > 0)
+ {
+ // Finish up on whatever is left
+ MarkNextChange();
+ }
+
+ return m_changes.ToArray();
+ }
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Retrieves all of the changes marked by the class in the reverse order
+ /// </summary>
+ //*********************************************************************
+ public IDiffChange[] ReverseChanges
+ {
+ get
+ {
+ if (m_originalCount > 0 || m_modifiedCount > 0)
+ {
+ // Finish up on whatever is left
+ MarkNextChange();
+ }
+
+ m_changes.Reverse();
+ return m_changes.ToArray();
+ }
+ }
+
+ // Member variables
+ private int m_originalCount;
+ private int m_modifiedCount;
+ private List<IDiffChange> m_changes;
+ private int m_originalStart;
+ private int m_modifiedStart;
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// A base for classes which compute the differences between two input sequences.
+ /// </summary>
+ //*************************************************************************
+ public abstract class DiffFinder<T> : IDisposable
+ {
+ //*************************************************************************
+ /// <summary>
+ /// The original sequence
+ /// </summary>
+ //*************************************************************************
+ protected IList<T> OriginalSequence
+ {
+ get { return m_original; }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// The modified sequence
+ /// </summary>
+ //*************************************************************************
+ protected IList<T> ModifiedSequence
+ {
+ get { return m_modified; }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// The element comparer
+ /// </summary>
+ //*************************************************************************
+ protected IEqualityComparer<T> ElementComparer
+ {
+ get { return m_elementComparer; }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Disposes resources used by this DiffFinder
+ /// </summary>
+ //*************************************************************************
+ public virtual void Dispose()
+ {
+ if (m_originalIds != null)
+ {
+ m_originalIds = null;
+ }
+ if (m_modifiedIds != null)
+ {
+ m_modifiedIds = null;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Returns true if the specified original and modified elements are equal.
+ /// </summary>
+ /// <param name="originalIndex">The index of the original element</param>
+ /// <param name="modifiedIndex">The index of the modified element</param>
+ /// <returns>True if the specified elements are equal</returns>
+ //*********************************************************************
+ protected bool ElementsAreEqual(int originalIndex, int modifiedIndex)
+ {
+ return ElementsAreEqual(originalIndex, true, modifiedIndex, false);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Returns true if the two specified original elements are equal.
+ /// </summary>
+ /// <param name="firstIndex">The index of the first original element</param>
+ /// <param name="secondIndex">The index of the second original element</param>
+ /// <returns>True if the specified elements are equal</returns>
+ //*********************************************************************
+ protected bool OriginalElementsAreEqual(int firstIndex, int secondIndex)
+ {
+ return ElementsAreEqual(firstIndex, true, secondIndex, true);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Returns true if the two specified modified elements are equal.
+ /// </summary>
+ /// <param name="firstIndex">The index of the first modified element</param>
+ /// <param name="secondIndex">The index of the second modified element</param>
+ /// <returns>True if the specified elements are equal</returns>
+ //*********************************************************************
+ protected bool ModifiedElementsAreEqual(int firstIndex, int secondIndex)
+ {
+ return ElementsAreEqual(firstIndex, false, secondIndex, false);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Returns true if the specified elements are equal.
+ /// </summary>
+ /// <param name="firstIndex">The index of the first element</param>
+ /// <param name="firstIsOriginal">True if the first element is an original
+ /// element, false if modified element</param>
+ /// <param name="secondIndex">The index of the second element</param>
+ /// <param name="secondIsOriginal">True if the second element is an original
+ /// element, false if modified element</param>
+ /// <returns>True if the specified elements are equal</returns>
+ //*********************************************************************
+ private bool ElementsAreEqual(int firstIndex, bool firstIsOriginal,
+ int secondIndex, bool secondIsOriginal)
+ {
+ int firstId = firstIsOriginal ? m_originalIds[firstIndex] : m_modifiedIds[firstIndex];
+ int secondId = secondIsOriginal ? m_originalIds[secondIndex] : m_modifiedIds[secondIndex];
+ if (firstId != 0 && secondId != 0)
+ {
+ return firstId == secondId;
+ }
+ else
+ {
+ T firstElement = firstIsOriginal ? OriginalSequence[firstIndex] : ModifiedSequence[firstIndex];
+ T secondElement = secondIsOriginal ? OriginalSequence[secondIndex] : ModifiedSequence[secondIndex];
+ return ElementComparer.Equals(firstElement, secondElement);
+ }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// This method hashes element groups within the specified range
+ /// and assigns unique identifiers to identical elements.
+ ///
+ /// This greatly speeds up element comparison as we may compare integers
+ /// instead of having to look at element content.
+ /// </summary>
+ //*************************************************************************
+ private void ComputeUniqueIdentifiers(int originalStart, int originalEnd,
+ int modifiedStart, int modifiedEnd)
+ {
+ Debug.Assert(originalStart >= 0 && originalEnd < OriginalSequence.Count, "Original range is invalid");
+ Debug.Assert(modifiedStart >= 0 && modifiedEnd < ModifiedSequence.Count, "Modified range is invalid");
+
+ // Create a new hash table for unique elements from the original
+ // sequence.
+ Dictionary<T, int> hashTable = new Dictionary<T, int>(OriginalSequence.Count + ModifiedSequence.Count,
+ ElementComparer);
+ int currentUniqueId = 1;
+
+ // Fill up the hash table for unique elements
+ for (int i = originalStart; i <= originalEnd; i++)
+ {
+ T originalElement = OriginalSequence[i];
+ if (!hashTable.TryGetValue(originalElement, out m_originalIds[i]))
+ {
+ // No entry in the hashtable so this is a new unique element.
+ // Assign the element a new unique identifier and add it to the
+ // hash table
+ m_originalIds[i] = currentUniqueId++;
+ hashTable.Add(originalElement, m_originalIds[i]);
+ }
+ }
+
+ // Now match up modified elements
+ for (int i = modifiedStart; i <= modifiedEnd; i++)
+ {
+ T modifiedElement = ModifiedSequence[i];
+ if (!hashTable.TryGetValue(modifiedElement, out m_modifiedIds[i]))
+ {
+ m_modifiedIds[i] = currentUniqueId++;
+ hashTable.Add(modifiedElement, m_modifiedIds[i]);
+ }
+ }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Computes the differences between the given original and modified sequences.
+ /// </summary>
+ /// <param name="original">The original sequence.</param>
+ /// <param name="modified">The modified sequence.</param>
+ /// <param name="elementComparer">The diff element comparer.</param>
+ /// <returns>The set of differences between the sequences.</returns>
+ //*************************************************************************
+ public IDiffChange[] Diff(IList<T> original, IList<T> modified, IEqualityComparer<T> elementComparer)
+ {
+ return Diff(original, modified, elementComparer, null);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Computes the differences between the given original and modified sequences.
+ /// </summary>
+ /// <param name="original">The original sequence.</param>
+ /// <param name="modified">The modified sequence.</param>
+ /// <param name="elementComparer">The diff element comparer.</param>
+ /// <returns>The set of differences between the sequences.</returns>
+ //*************************************************************************
+ public IDiffChange[] Diff(IList<T> original, IList<T> modified, IEqualityComparer<T> elementComparer,
+ ContinueDifferencePredicate<T> predicate)
+ {
+ Debug.Assert(original != null, "original is null");
+ Debug.Assert(modified != null, "modified is null");
+ Debug.Assert(elementComparer != null, "elementComparer is null");
+
+ m_original = original;
+ m_modified = modified;
+ m_elementComparer = elementComparer;
+ m_predicate = predicate;
+
+ m_originalIds = new int[OriginalSequence.Count];
+ m_modifiedIds = new int[ModifiedSequence.Count];
+
+ int originalStart = 0;
+ int originalEnd = OriginalSequence.Count - 1;
+ int modifiedStart = 0;
+ int modifiedEnd = ModifiedSequence.Count - 1;
+
+ // Find the start of the differences
+ while (originalStart <= originalEnd && modifiedStart <= modifiedEnd &&
+ ElementsAreEqual(originalStart, modifiedStart))
+ {
+ originalStart++;
+ modifiedStart++;
+ }
+
+ // Find the end of the differences
+ while (originalEnd >= originalStart && modifiedEnd >= modifiedStart &&
+ ElementsAreEqual(originalEnd, modifiedEnd))
+ {
+ originalEnd--;
+ modifiedEnd--;
+ }
+
+ // In the very special case where one of the ranges is negative
+ // Either the sequences are identical, or there is exactly one insertion
+ // or there is exactly one deletion. We have no need to compute the diffs.
+ if (originalStart > originalEnd || modifiedStart > modifiedEnd)
+ {
+ IDiffChange[] changes;
+
+ if (modifiedStart <= modifiedEnd)
+ {
+ Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd");
+
+ // All insertions
+ changes = new IDiffChange[1];
+ changes[0] = new DiffChange(originalStart, 0,
+ modifiedStart,
+ modifiedEnd - modifiedStart + 1);
+ }
+ else if (originalStart <= originalEnd)
+ {
+ Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd");
+
+ // All deletions
+ changes = new IDiffChange[1];
+ changes[0] = new DiffChange(originalStart,
+ originalEnd - originalStart + 1,
+ modifiedStart, 0);
+ }
+ else
+ {
+ Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd");
+ Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd");
+
+ // Identical sequences - No differences
+ changes = new IDiffChange[0];
+ }
+
+ return changes;
+ }
+
+ // Now that we have our bounds, calculate unique ids for all
+ // IDiffElements in the bounded range. That way, we speed up
+ // element comparisons during the diff computation.
+ ComputeUniqueIdentifiers(originalStart, originalEnd,
+ modifiedStart, modifiedEnd);
+
+ // Compute the diffences on the bounded range
+ return ComputeDiff(originalStart, originalEnd,
+ modifiedStart, modifiedEnd);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Computes the differences between the original and modified input
+ /// sequences on the bounded range.
+ /// </summary>
+ /// <returns>An array of the differences between the two input
+ /// sequences.</returns>
+ //*************************************************************************
+ protected abstract IDiffChange[] ComputeDiff(int originalStart, int originalEnd,
+ int modifiedStart, int modifiedEnd);
+
+ //*************************************************************************
+ /// <summary>
+ /// Gets the LCS Diff implementation.
+ /// </summary>
+ //*************************************************************************
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lcs")]
+ public static DiffFinder<T> LcsDiff
+ {
+ get { return new LcsDiff<T>(); }
+ }
+
+ protected ContinueDifferencePredicate<T> ContinueDifferencePredicate
+ {
+ get
+ {
+ return m_predicate;
+ }
+ }
+
+ // Member variables
+ private IList<T> m_original;
+ private IList<T> m_modified;
+ private int[] m_originalIds;
+ private int[] m_modifiedIds;
+ private IEqualityComparer<T> m_elementComparer;
+
+ //Early termination predicate
+ private ContinueDifferencePredicate<T> m_predicate;
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs
new file mode 100644
index 0000000..c70ca9d
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs
@@ -0,0 +1,801 @@
+//
+// 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.
+//
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+//*************************************************************************
+// The code from this point on is a soure-port of the TFS diff algorithm, to be available
+// in cases where we don't have access to the TFS diff assembly (i.e. outside of Visual Studio).
+//*************************************************************************
+
+namespace Microsoft.TeamFoundation.Diff.Copy
+{
+ //*************************************************************************
+ /// <summary>
+ /// An implementation of the difference algorithm described in
+ /// "An O(ND) Difference Algorithm and its Variations" by Eugene W. Myers
+ /// </summary>
+ //*************************************************************************
+ internal class LcsDiff<T> : DiffFinder<T>
+ {
+ //*************************************************************************
+ /// <summary>
+ /// Constructs the DiffFinder
+ /// </summary>
+ //*************************************************************************
+ public LcsDiff()
+ : base()
+ {
+ m_forwardHistory = new List<int[]>(MaxDifferencesHistory);
+ m_reverseHistory = new List<int[]>(MaxDifferencesHistory);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Disposes resources uses by this DiffFinder
+ /// </summary>
+ //*************************************************************************
+ public override void Dispose()
+ {
+ base.Dispose();
+ if (m_forwardHistory != null)
+ {
+ m_forwardHistory = null;
+ }
+ if (m_reverseHistory != null)
+ {
+ m_reverseHistory = null;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Computes the differences between the original and modified input
+ /// sequences on the bounded range.
+ /// </summary>
+ /// <returns>An array of the differences between the two input
+ /// sequences.</returns>
+ //*************************************************************************
+ protected override IDiffChange[] ComputeDiff(int originalStart, int originalEnd,
+ int modifiedStart, int modifiedEnd)
+ {
+ bool quitEarly;
+ IDiffChange[] changes = ComputeDiffRecursive(originalStart, originalEnd, modifiedStart, modifiedEnd, out quitEarly);
+
+ // We have to clean up the computed diff to be more intuitive
+ // but it turns out this cannot be done correctly until the entire set
+ // of diffs have been computed
+ return ShiftChanges(changes);
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Private helper method which computes the differences on the bounded range
+ /// recursively.
+ /// </summary>
+ /// <returns>An array of the differences between the two input
+ /// sequences.</returns>
+ //*************************************************************************
+ private IDiffChange[] ComputeDiffRecursive(int originalStart, int originalEnd,
+ int modifiedStart, int modifiedEnd,
+ out bool quitEarly)
+ {
+ quitEarly = false;
+
+ // Find the start of the differences
+ while (originalStart <= originalEnd && modifiedStart <= modifiedEnd &&
+ ElementsAreEqual(originalStart, modifiedStart))
+ {
+ originalStart++;
+ modifiedStart++;
+ }
+
+ // Find the end of the differences
+ while (originalEnd >= originalStart && modifiedEnd >= modifiedStart &&
+ ElementsAreEqual(originalEnd, modifiedEnd))
+ {
+ originalEnd--;
+ modifiedEnd--;
+ }
+
+ // In the special case where we either have all insertions or all deletions or the sequences are identical
+ if (originalStart > originalEnd || modifiedStart > modifiedEnd)
+ {
+ IDiffChange[] changes;
+
+ if (modifiedStart <= modifiedEnd)
+ {
+ Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd");
+
+ // All insertions
+ changes = new IDiffChange[1];
+ changes[0] = new DiffChange(originalStart, 0,
+ modifiedStart,
+ modifiedEnd - modifiedStart + 1);
+ }
+ else if (originalStart <= originalEnd)
+ {
+ Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd");
+
+ // All deletions
+ changes = new IDiffChange[1];
+ changes[0] = new DiffChange(originalStart,
+ originalEnd - originalStart + 1,
+ modifiedStart, 0);
+ }
+ else
+ {
+ Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd");
+ Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd");
+
+ // Identical sequences - No differences
+ changes = new IDiffChange[0];
+ }
+
+ return changes;
+ }
+
+ // This problem can be solved using the Divide-And-Conquer technique.
+ int midOriginal, midModified;
+ IDiffChange[] result = ComputeRecursionPoint(originalStart, originalEnd, modifiedStart, modifiedEnd,
+ out midOriginal, out midModified, out quitEarly);
+
+ if (result != null)
+ {
+ // Result is not-null when there was enough memory to compute the changes while
+ // searching for the recursion point
+ return result;
+ }
+ else if (!quitEarly)
+ {
+ // We can break the problem down recursively by finding the changes in the
+ // First Half: (originalStart, modifiedStart) to (midOriginal, midModified)
+ // Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd)
+ // NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point
+ IDiffChange[] leftChanges = ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, out quitEarly);
+ IDiffChange[] rightChanges = new IDiffChange[0];
+
+ if (!quitEarly)
+ {
+ rightChanges = ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd, out quitEarly);
+ }
+ else
+ {
+ // We did't have time to finish the first half, so we don't have time to compute this half.
+ // Consider the entire rest of the sequence different.
+ rightChanges = new DiffChange[]
+ {
+ new DiffChange(midOriginal + 1, originalEnd - (midOriginal + 1) + 1,
+ midModified + 1, modifiedEnd - (midModified + 1) + 1)
+ };
+ }
+
+ return ConcatenateChanges(leftChanges, rightChanges);
+ }
+
+ // If we hit here, we quit early, and so can't return anything meaningful
+ return new DiffChange[]
+ {
+ new DiffChange(originalStart, originalEnd -originalStart + 1,
+ modifiedStart, modifiedEnd - modifiedStart + 1)
+ };
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Given the range to compute the diff on, this method finds the point:
+ /// (midOriginal, midModified)
+ /// that exists in the middle of the LCS of the two sequences and
+ /// is the point at which the LCS problem may be broken down recursively.
+ /// This method will try to keep the LCS trace in memory. If the LCS recursion
+ /// point is calculated and the full trace is available in memory, then this method
+ /// will return the change list.
+ /// </summary>
+ /// <param name="originalStart">The start bound of the original sequence range</param>
+ /// <param name="originalEnd">The end bound of the original sequence range</param>
+ /// <param name="modifiedStart">The start bound of the modified sequence range</param>
+ /// <param name="modifiedEnd">The end bound of the modified sequence range</param>
+ /// <param name="midOriginal">The middle point of the original sequence range</param>
+ /// <param name="midModified">The middle point of the modified sequence range</param>
+ /// <returns>The diff changes, if available, otherwise null</returns>
+ //*************************************************************************
+ private IDiffChange[] ComputeRecursionPoint(int originalStart, int originalEnd,
+ int modifiedStart, int modifiedEnd,
+ out int midOriginal, out int midModified,
+ out bool quitEarly)
+ {
+ int originalIndex, modifiedIndex;
+ int diagonalForwardStart = 0, diagonalForwardEnd = 0;
+ int diagonalReverseStart = 0, diagonalReverseEnd = 0;
+ int numDifferences;
+
+ // To traverse the edit graph and produce the proper LCS, our actual
+ // start position is just outside the given boundary
+ originalStart--;
+ modifiedStart--;
+
+ // We set these up to make the compiler happy, but they will
+ // be replaced before we return with the actual recursion point
+ midOriginal = 0;
+ midModified = 0;
+
+ // Clear out the history
+ m_forwardHistory.Clear();
+ m_reverseHistory.Clear();
+
+ // Each cell in the two arrays corresponds to a diagonal in the edit graph.
+ // The integer value in the cell represents the originalIndex of the furthest
+ // reaching point found so far that ends in that diagonal.
+ // The modifiedIndex can be computed mathematically from the originalIndex and the diagonal number.
+ int maxDifferences = (originalEnd - originalStart) + (modifiedEnd - modifiedStart);
+ int numDiagonals = maxDifferences + 1;
+ int[] forwardPoints = new int[numDiagonals];
+ int[] reversePoints = new int[numDiagonals];
+ // diagonalForwardBase: Index into forwardPoints of the diagonal which passes through (originalStart, modifiedStart)
+ // diagonalReverseBase: Index into reversePoints of the diagonal which passes through (originalEnd, modifiedEnd)
+ int diagonalForwardBase = (modifiedEnd - modifiedStart);
+ int diagonalReverseBase = (originalEnd - originalStart);
+ // diagonalForwardOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
+ // diagonal number (relative to diagonalForwardBase)
+ // diagonalReverseOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the
+ // diagonal number (relative to diagonalReverseBase)
+ int diagonalForwardOffset = (originalStart - modifiedStart);
+ int diagonalReverseOffset = (originalEnd - modifiedEnd);
+
+ // delta: The difference between the end diagonal and the start diagonal. This is used to relate diagonal numbers
+ // relative to the start diagonal with diagonal numbers relative to the end diagonal.
+ // The Even/Oddn-ness of this delta is important for determining when we should check for overlap
+ int delta = diagonalReverseBase - diagonalForwardBase;
+ bool deltaIsEven = (delta % 2 == 0);
+
+ // Here we set up the start and end points as the furthest points found so far
+ // in both the forward and reverse directions, respectively
+ forwardPoints[diagonalForwardBase] = originalStart;
+ reversePoints[diagonalReverseBase] = originalEnd;
+
+ // Remember if we quit early, and thus need to do a best-effort result instead of a real result.
+ quitEarly = false;
+
+ // A couple of points:
+ // --With this method, we iterate on the number of differences between the two sequences.
+ // The more differences there actually are, the longer this will take.
+ // --Also, as the number of differences increases, we have to search on diagonals further
+ // away from the reference diagonal (which is diagonalForwardBase for forward, diagonalReverseBase for reverse).
+ // --We extend on even diagonals (relative to the reference diagonal) only when numDifferences
+ // is even and odd diagonals only when numDifferences is odd.
+ for (numDifferences = 1; numDifferences <= (maxDifferences / 2) + 1; numDifferences++)
+ {
+ int furthestOriginalIndex = 0;
+ int furthestModifiedIndex = 0;
+
+ // Run the algorithm in the forward direction
+ diagonalForwardStart = ClipDiagonalBound(diagonalForwardBase - numDifferences,
+ numDifferences,
+ diagonalForwardBase,
+ numDiagonals);
+ diagonalForwardEnd = ClipDiagonalBound(diagonalForwardBase + numDifferences,
+ numDifferences,
+ diagonalForwardBase,
+ numDiagonals);
+ for (int diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2)
+ {
+ // STEP 1: We extend the furthest reaching point in the present diagonal
+ // by looking at the diagonals above and below and picking the one whose point
+ // is further away from the start point (originalStart, modifiedStart)
+ if (diagonal == diagonalForwardStart || (diagonal < diagonalForwardEnd &&
+ forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1]))
+ {
+ originalIndex = forwardPoints[diagonal + 1];
+ }
+ else
+ {
+ originalIndex = forwardPoints[diagonal - 1] + 1;
+ }
+ modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset;
+
+ // Save the current originalIndex so we can test for false overlap in step 3
+ int tempOriginalIndex = originalIndex;
+
+ // STEP 2: We can continue to extend the furthest reaching point in the present diagonal
+ // so long as the elements are equal.
+ while (originalIndex < originalEnd && modifiedIndex < modifiedEnd
+ && ElementsAreEqual(originalIndex + 1, modifiedIndex + 1))
+ {
+ originalIndex++;
+ modifiedIndex++;
+ }
+ forwardPoints[diagonal] = originalIndex;
+
+ if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex)
+ {
+ furthestOriginalIndex = originalIndex;
+ furthestModifiedIndex = modifiedIndex;
+ }
+
+ // STEP 3: If delta is odd (overlap first happens on forward when delta is odd)
+ // and diagonal is in the range of reverse diagonals computed for numDifferences-1
+ // (the previous iteration; we havent computed reverse diagonals for numDifferences yet)
+ // then check for overlap.
+ if (!deltaIsEven && Math.Abs(diagonal - diagonalReverseBase) <= (numDifferences - 1))
+ {
+ if (originalIndex >= reversePoints[diagonal])
+ {
+ midOriginal = originalIndex;
+ midModified = modifiedIndex;
+
+ if (tempOriginalIndex <= reversePoints[diagonal]
+ && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1))
+ {
+ // BINGO! We overlapped, and we have the full trace in memory!
+ goto WALKTRACE;
+ }
+ else
+ {
+ // Either false overlap, or we didn't have enough memory for the full trace
+ // Just return the recursion point
+ return null;
+ }
+ }
+ }
+ }
+
+ // Check to see if we should be quitting early, before moving on to the next iteration.
+ int matchLengthOfLongest =
+ ((furthestOriginalIndex - originalStart) + (furthestModifiedIndex - modifiedStart) - numDifferences) / 2;
+
+ if (ContinueDifferencePredicate != null &&
+ !ContinueDifferencePredicate(furthestOriginalIndex, OriginalSequence, matchLengthOfLongest))
+ {
+ // We can't finish, so skip ahead to generating a result from what we have.
+ quitEarly = true;
+
+ // Use the furthest distance we got in the forward direction.
+ midOriginal = furthestOriginalIndex;
+ midModified = furthestModifiedIndex;
+
+ if (matchLengthOfLongest > 0 && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1))
+ {
+ // Enough of the history is in memory to walk it backwards
+ goto WALKTRACE;
+ }
+ else
+ {
+ // We didn't actually remember enough of the history.
+
+ //Since we are quiting the diff early, we need to shift back the originalStart and modified start
+ //back into the boundary limits since we decremented their value above beyond the boundary limit.
+ originalStart++;
+ modifiedStart++;
+
+ return new DiffChange[]
+ {
+ new DiffChange(originalStart, originalEnd - originalStart + 1,
+ modifiedStart, modifiedEnd - modifiedStart + 1)
+ };
+ }
+ }
+
+ // Run the algorithm in the reverse direction
+ diagonalReverseStart = ClipDiagonalBound(diagonalReverseBase - numDifferences,
+ numDifferences,
+ diagonalReverseBase,
+ numDiagonals);
+ diagonalReverseEnd = ClipDiagonalBound(diagonalReverseBase + numDifferences,
+ numDifferences,
+ diagonalReverseBase,
+ numDiagonals);
+ for (int diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2)
+ {
+ // STEP 1: We extend the furthest reaching point in the present diagonal
+ // by looking at the diagonals above and below and picking the one whose point
+ // is further away from the start point (originalEnd, modifiedEnd)
+ if (diagonal == diagonalReverseStart || (diagonal < diagonalReverseEnd &&
+ reversePoints[diagonal - 1] >= reversePoints[diagonal + 1]))
+ {
+ originalIndex = reversePoints[diagonal + 1] - 1;
+ }
+ else
+ {
+ originalIndex = reversePoints[diagonal - 1];
+ }
+ modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset;
+
+ // Save the current originalIndex so we can test for false overlap
+ int tempOriginalIndex = originalIndex;
+
+ // STEP 2: We can continue to extend the furthest reaching point in the present diagonal
+ // as long as the elements are equal.
+ while (originalIndex > originalStart && modifiedIndex > modifiedStart
+ && ElementsAreEqual(originalIndex, modifiedIndex))
+ {
+ originalIndex--;
+ modifiedIndex--;
+ }
+ reversePoints[diagonal] = originalIndex;
+
+ // STEP 4: If delta is even (overlap first happens on reverse when delta is even)
+ // and diagonal is in the range of forward diagonals computed for numDifferences
+ // then check for overlap.
+ if (deltaIsEven && Math.Abs(diagonal - diagonalForwardBase) <= numDifferences)
+ {
+ if (originalIndex <= forwardPoints[diagonal])
+ {
+ midOriginal = originalIndex;
+ midModified = modifiedIndex;
+
+ if (tempOriginalIndex >= forwardPoints[diagonal]
+ && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1))
+ {
+ // BINGO! We overlapped, and we have the full trace in memory!
+ goto WALKTRACE;
+ }
+ else
+ {
+ // Either false overlap, or we didn't have enough memory for the full trace
+ // Just return the recursion point
+ return null;
+ }
+ }
+ }
+ }
+
+ // Save current vectors to history before the next iteration
+ if (numDifferences <= MaxDifferencesHistory)
+ {
+ // We are allocating space for one extra int, which we fill with
+ // the index of the diagonal base index
+ int[] temp = new int[diagonalForwardEnd - diagonalForwardStart + 2];
+ temp[0] = diagonalForwardBase - diagonalForwardStart + 1;
+ Array.Copy(forwardPoints, diagonalForwardStart, temp, 1, diagonalForwardEnd - diagonalForwardStart + 1);
+ m_forwardHistory.Add(temp);
+
+ temp = new int[diagonalReverseEnd - diagonalReverseStart + 2];
+ temp[0] = diagonalReverseBase - diagonalReverseStart + 1;
+ Array.Copy(reversePoints, diagonalReverseStart, temp, 1, diagonalReverseEnd - diagonalReverseStart + 1);
+ m_reverseHistory.Add(temp);
+ }
+ }
+
+ // If we got here, then we have the full trace in history. We just have to convert it to a change list
+ // NOTE: This part is a bit messy
+ WALKTRACE: IDiffChange[] forwardChanges, reverseChanges;
+
+ // First, walk backward through the forward diagonals history
+ using (DiffChangeHelper changeHelper = new DiffChangeHelper())
+ {
+ int diagonalMin = diagonalForwardStart;
+ int diagonalMax = diagonalForwardEnd;
+ int diagonalRelative = (midOriginal - midModified) - diagonalForwardOffset;
+ int lastOriginalIndex = Int32.MinValue;
+ int historyIndex = m_forwardHistory.Count - 1;
+
+ do
+ {
+ // Get the diagonal index from the relative diagonal number
+ int diagonal = diagonalRelative + diagonalForwardBase;
+
+ // Figure out where we came from
+ if (diagonal == diagonalMin || (diagonal < diagonalMax &&
+ forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1]))
+ {
+ // Vertical line (the element is an insert)
+ originalIndex = forwardPoints[diagonal + 1];
+ modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
+ if (originalIndex < lastOriginalIndex)
+ {
+ changeHelper.MarkNextChange();
+ }
+ lastOriginalIndex = originalIndex;
+ changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex);
+ diagonalRelative = (diagonal + 1) - diagonalForwardBase; //Setup for the next iteration
+ }
+ else
+ {
+ // Horizontal line (the element is a deletion)
+ originalIndex = forwardPoints[diagonal - 1] + 1;
+ modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
+ if (originalIndex < lastOriginalIndex)
+ {
+ changeHelper.MarkNextChange();
+ }
+ lastOriginalIndex = originalIndex - 1;
+ changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1);
+ diagonalRelative = (diagonal - 1) - diagonalForwardBase; //Setup for the next iteration
+ }
+
+ if (historyIndex >= 0)
+ {
+ forwardPoints = m_forwardHistory[historyIndex];
+ diagonalForwardBase = forwardPoints[0]; //We stored this in the first spot
+ diagonalMin = 1;
+ diagonalMax = forwardPoints.Length - 1;
+ }
+ } while (--historyIndex >= -1);
+
+ // Ironically, we get the forward changes as the reverse of the
+ // order we added them since we technically added them backwards
+ forwardChanges = changeHelper.ReverseChanges;
+ }
+
+ if (quitEarly)
+ {
+ // Since we did quit early, assume everything after the midOriginal/midModified point is a diff
+
+ int originalStartPoint = midOriginal + 1;
+ int modifiedStartPoint = midModified + 1;
+
+ if (forwardChanges != null && forwardChanges.Length > 0)
+ {
+ IDiffChange lastForwardChange = forwardChanges[forwardChanges.Length - 1];
+ originalStartPoint = Math.Max(originalStartPoint, lastForwardChange.OriginalEnd);
+ modifiedStartPoint = Math.Max(modifiedStartPoint, lastForwardChange.ModifiedEnd);
+ }
+
+ reverseChanges = new DiffChange[]
+ {
+ new DiffChange(originalStartPoint, originalEnd - originalStartPoint + 1,
+ modifiedStartPoint, modifiedEnd - modifiedStartPoint + 1)
+ };
+ }
+ else
+ {
+ // Now walk backward through the reverse diagonals history
+ using (DiffChangeHelper changeHelper = new DiffChangeHelper())
+ {
+ int diagonalMin = diagonalReverseStart;
+ int diagonalMax = diagonalReverseEnd;
+ int diagonalRelative = (midOriginal - midModified) - diagonalReverseOffset;
+ int lastOriginalIndex = Int32.MaxValue;
+ int historyIndex = (deltaIsEven) ? m_reverseHistory.Count - 1
+ : m_reverseHistory.Count - 2;
+
+ do
+ {
+ // Get the diagonal index from the relative diagonal number
+ int diagonal = diagonalRelative + diagonalReverseBase;
+
+ // Figure out where we came from
+ if (diagonal == diagonalMin || (diagonal < diagonalMax &&
+ reversePoints[diagonal - 1] >= reversePoints[diagonal + 1]))
+ {
+ // Horizontal line (the element is a deletion))
+ originalIndex = reversePoints[diagonal + 1] - 1;
+ modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
+ if (originalIndex > lastOriginalIndex)
+ {
+ changeHelper.MarkNextChange();
+ }
+ lastOriginalIndex = originalIndex + 1;
+ changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1);
+ diagonalRelative = (diagonal + 1) - diagonalReverseBase; //Setup for the next iteration
+ }
+ else
+ {
+ // Vertical line (the element is an insertion)
+ originalIndex = reversePoints[diagonal - 1];
+ modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
+ if (originalIndex > lastOriginalIndex)
+ {
+ changeHelper.MarkNextChange();
+ }
+ lastOriginalIndex = originalIndex;
+ changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1);
+ diagonalRelative = (diagonal - 1) - diagonalReverseBase; //Setup for the next iteration
+ }
+
+ if (historyIndex >= 0)
+ {
+ reversePoints = m_reverseHistory[historyIndex];
+ diagonalReverseBase = reversePoints[0]; //We stored this in the first spot
+ diagonalMin = 1;
+ diagonalMax = reversePoints.Length - 1;
+ }
+ } while (--historyIndex >= -1);
+
+ // There are cases where the reverse history will find diffs that
+ // are correct, but not intuitive, so we need shift them.
+ reverseChanges = changeHelper.Changes;
+ }
+ }
+
+ return ConcatenateChanges(forwardChanges, reverseChanges);
+ }
+
+ //*********************************************************************
+ /// <summary>
+ /// Shifts the given changes to provide a more intuitive diff.
+ /// While the first element in a diff matches the first element after the diff,
+ /// we shift the diff down.
+ /// </summary>
+ /// <param name="changes">The list of changes to shift</param>
+ /// <returns>The shifted changes</returns>
+ //*********************************************************************
+ private IDiffChange[] ShiftChanges(IDiffChange[] changes)
+ {
+ // Shift all the changes first
+ for (int i = 0; i < changes.Length; i++)
+ {
+ Debug.Assert(changes[i] is DiffChange, "change is not a DiffChange");
+
+ DiffChange change = changes[i] as DiffChange;
+ int originalStop = (i < changes.Length - 1) ? changes[i + 1].OriginalStart : OriginalSequence.Count;
+ int modifiedStop = (i < changes.Length - 1) ? changes[i + 1].ModifiedStart : ModifiedSequence.Count;
+ bool checkOriginal = change.OriginalLength > 0;
+ bool checkModified = change.ModifiedLength > 0;
+
+ while (change.OriginalStart + change.OriginalLength < originalStop &&
+ change.ModifiedStart + change.ModifiedLength < modifiedStop &&
+ (!checkOriginal || OriginalElementsAreEqual(change.OriginalStart, change.OriginalStart + change.OriginalLength)) &&
+ (!checkModified || ModifiedElementsAreEqual(change.ModifiedStart, change.ModifiedStart + change.ModifiedLength)))
+ {
+ change.OriginalStart++;
+ change.ModifiedStart++;
+ }
+ }
+
+ // Build up the new list (we have to build a new list because we
+ // might have changes we can merge together now)
+ List<IDiffChange> result = new List<IDiffChange>(changes.Length);
+ IDiffChange mergedChange;
+ for (int i = 0; i < changes.Length; i++)
+ {
+ if (i < changes.Length - 1 && ChangesOverlap(changes[i], changes[i + 1], out mergedChange))
+ {
+ result.Add(mergedChange);
+ i++;
+ }
+ else
+ {
+ result.Add(changes[i]);
+ }
+ }
+
+ return result.ToArray();
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Concatentates the two input DiffChange lists and returns the resulting
+ /// list.
+ /// </summary>
+ /// <param name="left">The left changes</param>
+ /// <param name="right">The right changes</param>
+ /// <returns>The concatenated list</returns>
+ //*************************************************************************
+ private IDiffChange[] ConcatenateChanges(IDiffChange[] left, IDiffChange[] right)
+ {
+ IDiffChange mergedChange;
+
+ if (left.Length == 0 || right.Length == 0)
+ {
+ return (right.Length > 0) ? right : left;
+ }
+ else if (ChangesOverlap(left[left.Length - 1], right[0], out mergedChange))
+ {
+ // Since we break the problem down recursively, it is possible that we
+ // might recurse in the middle of a change thereby splitting it into
+ // two changes. Here in the combining stage, we detect and fuse those
+ // changes back together
+ IDiffChange[] result = new IDiffChange[left.Length + right.Length - 1];
+ Array.Copy(left, 0, result, 0, left.Length - 1);
+ result[left.Length - 1] = mergedChange;
+ Array.Copy(right, 1, result, left.Length, right.Length - 1);
+
+ return result;
+ }
+ else
+ {
+ IDiffChange[] result = new IDiffChange[left.Length + right.Length];
+ Array.Copy(left, 0, result, 0, left.Length);
+ Array.Copy(right, 0, result, left.Length, right.Length);
+
+ return result;
+ }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Returns true if the two changes overlap and can be merged into a single
+ /// change
+ /// </summary>
+ /// <param name="left">The left change</param>
+ /// <param name="right">The right change</param>
+ /// <param name="mergedChange">The merged change if the two overlap,
+ /// null otherwise</param>
+ /// <returns>True if the two changes overlap</returns>
+ //*************************************************************************
+ private bool ChangesOverlap(IDiffChange left, IDiffChange right, out IDiffChange mergedChange)
+ {
+ Debug.Assert(left.OriginalStart <= right.OriginalStart, "Left change is not less than or equal to right change");
+ Debug.Assert(left.ModifiedStart <= right.ModifiedStart, "Left change is not less than or equal to right change");
+
+ if (left.OriginalStart + left.OriginalLength >= right.OriginalStart
+ || left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart)
+ {
+ int originalStart = left.OriginalStart;
+ int originalLength = left.OriginalLength;
+ int modifiedStart = left.ModifiedStart;
+ int modifiedLength = left.ModifiedLength;
+
+ if (left.OriginalStart + left.OriginalLength >= right.OriginalStart)
+ {
+ originalLength = right.OriginalStart + right.OriginalLength - left.OriginalStart;
+ }
+ if (left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart)
+ {
+ modifiedLength = right.ModifiedStart + right.ModifiedLength - left.ModifiedStart;
+ }
+
+ mergedChange = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength);
+ return true;
+ }
+ else
+ {
+ mergedChange = null;
+ return false;
+ }
+ }
+
+ //*************************************************************************
+ /// <summary>
+ /// Helper method used to clip a diagonal index to the range of valid
+ /// diagonals. This also decides whether or not the diagonal index,
+ /// if it exceeds the boundary, should be clipped to the boundary or clipped
+ /// one inside the boundary depending on the Even/Odd status of the boundary
+ /// and numDifferences.
+ /// </summary>
+ /// <param name="diagonal">The index of the diagonal to clip.</param>
+ /// <param name="numDifferences">The current number of differences being
+ /// iterated upon.</param>
+ /// <param name="diagonalBaseIndex">The base reference diagonal.</param>
+ /// <param name="numDiagonals">The total number of diagonals.</param>
+ /// <returns>The clipped diagonal index.</returns>
+ //*************************************************************************
+ private int ClipDiagonalBound(int diagonal,
+ int numDifferences,
+ int diagonalBaseIndex,
+ int numDiagonals)
+ {
+ if (diagonal >= 0 && diagonal < numDiagonals)
+ {
+ // Nothing to clip, its in range
+ return diagonal;
+ }
+
+ // diagonalsBelow: The number of diagonals below the reference diagonal
+ // diagonalsAbove: The number of diagonals above the reference diagonal
+ int diagonalsBelow = diagonalBaseIndex;
+ int diagonalsAbove = numDiagonals - diagonalBaseIndex - 1;
+ bool diffEven = (numDifferences % 2 == 0);
+
+ if (diagonal < 0)
+ {
+ bool lowerBoundEven = (diagonalsBelow % 2 == 0);
+ return (diffEven == lowerBoundEven) ? 0 : 1;
+ }
+ else
+ {
+ bool upperBoundEven = (diagonalsAbove % 2 == 0);
+ return (diffEven == upperBoundEven) ? numDiagonals - 1 : numDiagonals - 2;
+ }
+ }
+
+ // Member variables
+ private List<int[]> m_forwardHistory;
+ private List<int[]> m_reverseHistory;
+
+ // Our total memory usage for storing history is (worst-case):
+ // 2 * [(MaxDifferencesHistory + 1) * (MaxDifferencesHistory + 1) - 1] * sizeof(int)
+ // 2 * [1448*1448 - 1] * 4 = 16773624 = 16MB
+ private const int MaxDifferencesHistory = 1447;
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs b/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs
new file mode 100644
index 0000000..5a27014
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs
@@ -0,0 +1,45 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Text;
+using Microsoft.VisualStudio.Text.Utilities;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ [Export(typeof(ITextDifferencingSelectorService))]
+ class TextDifferencingSelectorService : ITextDifferencingSelectorService
+ {
+ [ImportMany(typeof(ITextDifferencingService))]
+ internal List<Lazy<ITextDifferencingService, IContentTypeMetadata>> _textDifferencingServices { get; set; }
+
+ [Import]
+ internal IContentTypeRegistryService _contentTypeRegistryService { get; set; }
+
+ [Import]
+ internal GuardedOperations _guardedOperations { get; set; }
+
+ public ITextDifferencingService GetTextDifferencingService(IContentType contentType)
+ {
+ ITextDifferencingService service =
+ _guardedOperations.InvokeBestMatchingFactory
+ (_textDifferencingServices,
+ contentType,
+ differencingService => differencingService,
+ _contentTypeRegistryService, this);
+
+ return service ?? DefaultTextDifferencingService;
+ }
+
+ static DefaultTextDifferencingService _defaultTextDifferencingService = new DefaultTextDifferencingService();
+ public ITextDifferencingService DefaultTextDifferencingService { get { return _defaultTextDifferencingService; } }
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs
new file mode 100644
index 0000000..f7afc1c
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs
@@ -0,0 +1,270 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ internal interface ITokenizedStringListInternal : ITokenizedStringList
+ {
+ string OriginalSubstring(int startIndex, int length);
+ }
+
+ /// <summary>
+ /// Tokenizes the string into abutting and non-overlapping segments.
+ /// </summary>
+ /// <remarks>
+ /// This class implements IList so that it can be used with
+ /// <see cref="IDifferenceService" />, which finds the differences between two sequences represented
+ /// as ILists.
+ /// Most of the members of the IList interface are unimplemented. The only
+ /// implemented methods are the array accessor getter (operator []), Count,
+ /// and IsReadOnly.
+ /// </remarks>
+ internal abstract class TokenizedStringList : ITokenizedStringListInternal
+ {
+ protected List<Span> Tokens = new List<Span>();
+ private readonly string original;
+ private readonly SnapshotSpan originalSpan;
+
+ /// <summary>
+ /// Creates a tokenized string list from the original string.
+ /// Any derived class must initialize the Tokens list in its own constructor.
+ /// </summary>
+ /// <param name="original">The original string.</param>
+ protected TokenizedStringList(string original)
+ {
+ if (original == null)
+ throw new ArgumentNullException("original");
+
+ this.original = original;
+ }
+
+ protected TokenizedStringList(SnapshotSpan originalSpan)
+ {
+ this.originalSpan = originalSpan;
+ }
+
+ /// <summary>
+ /// The original string that was tokenized.
+ /// </summary>
+ public string Original
+ {
+ get
+ {
+ // A call to GetText() here could be very expensive in memory. Be careful!
+ return original ?? originalSpan.GetText();
+ }
+ }
+
+ internal int OriginalLength
+ {
+ get
+ {
+ return (original != null) ? original.Length : originalSpan.Length;
+ }
+ }
+
+ public string OriginalSubstring(int startIndex, int length)
+ {
+ if (original != null)
+ {
+ return original.Substring(startIndex, length);
+ }
+ else
+ {
+ ITextSnapshot snap = originalSpan.Snapshot;
+ return snap.GetText(originalSpan.Start.Position + startIndex, length);
+ }
+ }
+
+ /// <summary>
+ /// Maps the index of an element to its span in the original list.
+ /// </summary>
+ /// <param name="index">The index of the element in the element list.</param>
+ /// <returns>The span of the element.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">The specified index is either negative or exceeds the list's Count property.</exception>
+ /// <remarks>This method returns a zero-length span at the end of the string if index
+ /// is equal to the list's Count property.</remarks>
+ public Span GetElementInOriginal(int index)
+ {
+ //Pure support for backward compatibility
+ if (index == this.Count)
+ {
+ return new Span(this.OriginalLength, 0);
+ }
+
+ return this.Tokens[index];
+ }
+
+ /// <summary>
+ /// Maps a span of elements in this list to the span in the original list.
+ /// </summary>
+ /// <param name="span">The span of elements in the elements list.</param>
+ /// <returns>The span mapped onto the original list.</returns>
+ public Span GetSpanInOriginal(Span span)
+ {
+ //Pure support for backward compatibility
+ if (span.Start == this.Tokens.Count)
+ {
+ return new Span(this.OriginalLength, 0);
+ }
+
+ int start = this.Tokens[span.Start].Start;
+ int end = (span.Length == 0) ? start : this.Tokens[span.End - 1].End;
+
+ return Span.FromBounds(start, end);
+ }
+
+ /// <summary>
+ /// Gets a string of the given element.
+ /// </summary>
+ /// <param name="index">The index into the list of elements.</param>
+ /// <returns>The element, as a string.</returns>
+ /// <remarks>The setter of this property throws a NotImplementedException.</remarks>
+ public string this[int index]
+ {
+ get
+ {
+ // The out of range check will happen in GetElementInOriginal
+ Span span = GetElementInOriginal(index);
+ return this.OriginalSubstring(span.Start, span.Length);
+ }
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ internal char CharacterAt(int offset)
+ {
+ return (original != null) ? original[offset] : originalSpan.Snapshot[originalSpan.Start.Position + offset];
+ }
+
+ /// <summary>
+ /// The number of elements in the list.
+ /// </summary>
+ public int Count
+ {
+ get
+ {
+ return this.Tokens.Count;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether this list is read-only. It always returnes <c>true</c>.
+ /// </summary>
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+#region Not Implemented
+ /// <summary>
+ /// Not implemented
+ /// </summary>
+ /// <param name="item"></param>
+ /// <returns></returns>
+ public int IndexOf(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ /// <param name="index"></param>
+ /// <param name="item"></param>
+ public void Insert(int index, string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ /// <param name="index"></param>
+ public void RemoveAt(int index)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ /// <param name="item"></param>
+ public void Add(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ public void Clear()
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ public bool Contains(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ public void CopyTo(string[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Not implemented.
+ /// </summary>
+ public bool Remove(string item)
+ {
+ throw new NotSupportedException();
+ }
+
+#endregion
+
+#region IEnumerable<string> Members
+
+ /// <summary>
+ /// Gets the enumerator of type string.
+ /// </summary>
+ /// <returns>The enumerator of type string.</returns>
+ public IEnumerator<string> GetEnumerator()
+ {
+ for (int i = 0; i < Count; i++)
+ yield return this[i];
+ }
+
+#endregion
+
+#region IEnumerable Members
+
+ /// <summary>
+ /// Gets the untyped enumerator.
+ /// </summary>
+ /// <returns>The untyped enumerator.</returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+#endregion
+ }
+}
diff --git a/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs
new file mode 100644
index 0000000..e676e73
--- /dev/null
+++ b/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs
@@ -0,0 +1,132 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Globalization;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ // TODO: Replace the logic in this class with the upcoming Line/Word
+ // split utility.
+
+ /// <summary>
+ /// This is a word decomposition of the given string.
+ /// </summary>
+ internal sealed class WordDecompositionList : TokenizedStringList
+ {
+ public WordDecompositionList(string original, StringDifferenceOptions options)
+ : base(original)
+ {
+ this.CreateTokens(options, ignoreTrimWhiteSpace: false); // We never paid attention to trim whitespace for strings when doing line or word diffs.
+ }
+
+ public WordDecompositionList(SnapshotSpan original, StringDifferenceOptions options)
+ : base(original)
+ {
+ this.CreateTokens(options, options.IgnoreTrimWhiteSpace);
+ }
+
+ private void CreateTokens(StringDifferenceOptions options, bool ignoreTrimWhiteSpace)
+ {
+ int end = this.OriginalLength;
+ int i = 0;
+ int tokenStart = 0;
+ TokenType previousTokenType = TokenType.WhiteSpace;
+ bool skipPreviousToken = ignoreTrimWhiteSpace; // Assume that the 1st whitespace token is ignoreable if we're trimming whitespace.
+ while (i < end)
+ {
+ bool skipNextToken;
+ TokenType nextTokenType;
+ int breakLength = this.LengthOfLineBreak(i, end);
+ if (breakLength != 0)
+ {
+ nextTokenType = ignoreTrimWhiteSpace ? TokenType.WhiteSpace : TokenType.LineBreak;
+ skipNextToken = ignoreTrimWhiteSpace;
+ }
+ else
+ {
+ nextTokenType = GetTokenType(this.CharacterAt(i), options.WordSplitBehavior);
+ skipNextToken = (nextTokenType == TokenType.WhiteSpace) ? skipPreviousToken : false;
+ breakLength = 1;
+ }
+
+ if ((nextTokenType != previousTokenType) || (nextTokenType == TokenType.Symbol))
+ {
+ if ((tokenStart < i) && !skipPreviousToken)
+ {
+ this.Tokens.Add(new Span(tokenStart, i - tokenStart));
+ }
+
+ previousTokenType = nextTokenType;
+ tokenStart = i;
+ }
+
+ skipPreviousToken = skipNextToken;
+ i += breakLength;
+ }
+
+ if ((end == 0) || // 0-length sequences get a single token
+ !(ignoreTrimWhiteSpace && (previousTokenType == TokenType.WhiteSpace))) // act as if there is an implicit line break at the end of the line.
+ {
+ this.Tokens.Add(new Span(tokenStart, end - tokenStart));
+ }
+ }
+
+ private enum TokenType
+ {
+ LineBreak,
+ WhiteSpace,
+ Symbol,
+ Digit,
+ Letter,
+ Other,
+ };
+
+ private static TokenType GetTokenType(char c, WordSplitBehavior splitBehavior)
+ {
+ if (char.IsWhiteSpace(c))
+ return TokenType.WhiteSpace;
+
+ if (splitBehavior == WordSplitBehavior.WhiteSpace)
+ return TokenType.Other;
+
+ if (char.IsPunctuation(c) || char.IsSymbol(c))
+ return TokenType.Symbol;
+
+ if (splitBehavior == WordSplitBehavior.WhiteSpaceAndPunctuation)
+ return TokenType.Other;
+
+ if (char.IsDigit(c) || char.IsNumber(c))
+ return TokenType.Digit;
+
+ if (char.IsLetter(c))
+ return TokenType.Letter;
+
+ return TokenType.Other;
+ }
+
+ // This is roughly a copy of TextUtilities.LengthOfLineBreak (but using CharacterAt).
+ public int LengthOfLineBreak(int start, int end)
+ {
+ char c1 = this.CharacterAt(start);
+ if (c1 == '\r')
+ {
+ return ((++start < end) && (this.CharacterAt(start) == '\n')) ? 2 : 1;
+ }
+ else if ((c1 == '\n') || (c1 == '\u0085') ||
+ (c1 == '\u2028' /*unicode line separator*/) ||
+ (c1 == '\u2029' /*unicode paragraph separator*/))
+ {
+ return 1;
+ }
+ else
+ return 0;
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs
new file mode 100644
index 0000000..97d1369
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs
@@ -0,0 +1,261 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// The UndoPrimitive to take place on the Undo stack after a text buffer change. This is the simpler
+ /// version of the primitive that handles most common cases.
+ /// </summary>
+ internal class AfterTextBufferChangeUndoPrimitive : TextUndoPrimitive
+ {
+ // Think twice before adding any fields here! These objects are long-lived and consume considerable space.
+ // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below.
+ protected ITextUndoHistory _undoHistory;
+ protected int _newCaretIndex;
+ protected byte _newCaretAffinityByte;
+ protected bool _canUndo;
+
+ /// <summary>
+ /// Constructs a AfterTextBufferChangeUndoPrimitive.
+ /// </summary>
+ /// <param name="textView">
+ /// The text view that was responsible for causing this change.
+ /// </param>
+ /// <param name="undoHistory">
+ /// The <see cref="ITextUndoHistory" /> this primitive will be added to.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="textView"/> is null.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception>
+ public static AfterTextBufferChangeUndoPrimitive Create(ITextView textView, ITextUndoHistory undoHistory)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException("textView");
+ }
+ if (undoHistory == null)
+ {
+ throw new ArgumentNullException("undoHistory");
+ }
+
+ // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later.
+ if (!undoHistory.Properties.ContainsProperty(typeof(ITextView)))
+ {
+ undoHistory.Properties[typeof(ITextView)] = textView;
+ }
+
+ IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView);
+
+ CaretPosition caret = textView.Caret.Position;
+ int newCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition);
+ int newCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces;
+
+ VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint;
+ int newSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position);
+ int newSelectionAnchorVirtualSpaces = anchor.VirtualSpaces;
+
+ VirtualSnapshotPoint active = textView.Selection.ActivePoint;
+ int newSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position);
+ int newSelectionActiveVirtualSpaces = active.VirtualSpaces;
+
+ TextSelectionMode newSelectionMode = textView.Selection.Mode;
+
+ if (newCaretVirtualSpaces != 0 ||
+ newSelectionAnchorIndex != newCaretIndex ||
+ newSelectionAnchorVirtualSpaces != 0 ||
+ newSelectionActiveIndex != newCaretIndex ||
+ newSelectionActiveVirtualSpaces != 0 ||
+ newSelectionMode != TextSelectionMode.Stream)
+ {
+ return new GeneralAfterTextBufferChangeUndoPrimitive
+ (undoHistory, newCaretIndex, caret.Affinity, newCaretVirtualSpaces, newSelectionAnchorIndex,
+ newSelectionAnchorVirtualSpaces, newSelectionActiveIndex, newSelectionActiveVirtualSpaces, newSelectionMode);
+ }
+ else
+ {
+ return new AfterTextBufferChangeUndoPrimitive(undoHistory, newCaretIndex, caret.Affinity);
+ }
+ }
+
+ protected AfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity)
+ {
+ _undoHistory = undoHistory;
+ _newCaretIndex = caretIndex;
+ _newCaretAffinityByte = (byte)caretAffinity;
+ _canUndo = true;
+ }
+
+ // Internal empty constructor for unit testing.
+ internal AfterTextBufferChangeUndoPrimitive() { }
+
+ /// <summary>
+ /// The <see cref="ITextView"/> that this <see cref="AfterTextBufferChangeUndoPrimitive"/> is bound to.
+ /// </summary>
+ internal ITextView GetTextView()
+ {
+ ITextView view = null;
+ _undoHistory.Properties.TryGetProperty(typeof(ITextView), out view);
+ return view;
+ }
+
+ internal int CaretIndex
+ {
+ get { return _newCaretIndex; }
+ }
+
+ internal virtual int CaretVirtualSpace
+ {
+ get { return 0; }
+ }
+
+ #region ITextUndoPrimitive Members
+
+ /// <summary>
+ /// Returns true if operation can be undone, false otherwise.
+ /// </summary>
+ public override bool CanUndo
+ {
+ get { return _canUndo; }
+ }
+
+ /// <summary>
+ /// Returns true if operation can be redone, false otherwise.
+ /// </summary>
+ public override bool CanRedo
+ {
+ get { return !_canUndo; }
+ }
+
+ /// <summary>
+ /// Do the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception>
+ public override void Do()
+ {
+ // Validate, we shouldn't be allowed to undo
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException(Strings.CannotRedo);
+ }
+
+ // Set the new caret position and active selection
+ var view = this.GetTextView();
+ Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen.");
+ if (view != null && !view.IsClosed)
+ {
+ DoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view));
+ view.Caret.EnsureVisible();
+ }
+
+ _canUndo = true;
+ }
+
+ /// <summary>
+ /// Move the caret and restore the selection as part of the Redo operation.
+ /// </summary>
+ protected virtual void DoMoveCaretAndSelect(ITextView view, IMapEditToData map)
+ {
+ SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex));
+
+ view.Caret.MoveTo(newCaret, (PositionAffinity)_newCaretAffinityByte);
+ view.Selection.Clear();
+ }
+
+ /// <summary>
+ /// Undo the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Undo()
+ {
+ // Validate that we can undo this change
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException(Strings.CannotUndo);
+ }
+
+ // Currently, no action is done on the Undo. To restore the caret and selection after a text buffer undo, there is the BeforeTextBufferChangeUndoPrimitive.
+ // This undo should not do anything with the caret and selection, because we only want to reset them after the TextBuffer change has occurred.
+ // Therefore, we need to add this UndoPrimitive to the undo stack before the UndoPrimitive for the TextBuffer change. On an redo, the TextBuffer changed UndoPrimitive
+ // will fire it's Redo first, and than the Redo for this UndoPrimitive will fire.
+ // However, on an undo, the undo for this UndoPrimitive will be fired, and then the undo for the TextBuffer change UndoPrimitive. If we had set any caret placement/selection here (ie the old caret placement/selection),
+ // we may crash because the TextBuffer change has not occurred yet (ie you try to set the caret to be at CharacterIndex 1 when the TextBuffer is still empty).
+
+ _canUndo = false;
+ }
+
+ public override bool CanMerge(ITextUndoPrimitive older)
+ {
+ return false;
+ }
+ #endregion
+ }
+
+ /// <summary>
+ /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general
+ /// version of the primitive that handles all cases, including those involving selections and virtual space.
+ /// </summary>
+ internal class GeneralAfterTextBufferChangeUndoPrimitive : AfterTextBufferChangeUndoPrimitive
+ {
+ private int _newCaretVirtualSpaces;
+ private int _newSelectionAnchorIndex;
+ private int _newSelectionAnchorVirtualSpaces;
+ private int _newSelectionActiveIndex;
+ private int _newSelectionActiveVirtualSpaces;
+ private TextSelectionMode _newSelectionMode;
+
+ public GeneralAfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory,
+ int newCaretIndex,
+ PositionAffinity newCaretAffinity,
+ int newCaretVirtualSpaces,
+ int newSelectionAnchorIndex,
+ int newSelectionAnchorVirtualSpaces,
+ int newSelectionActiveIndex,
+ int newSelectionActiveVirtualSpaces,
+ TextSelectionMode newSelectionMode)
+ : base(undoHistory, newCaretIndex, newCaretAffinity)
+ {
+ _newCaretVirtualSpaces = newCaretVirtualSpaces;
+ _newSelectionAnchorIndex = newSelectionAnchorIndex;
+ _newSelectionAnchorVirtualSpaces = newSelectionAnchorVirtualSpaces;
+ _newSelectionActiveIndex = newSelectionActiveIndex;
+ _newSelectionActiveVirtualSpaces = newSelectionActiveVirtualSpaces;
+ _newSelectionMode = newSelectionMode;
+ }
+
+ internal override int CaretVirtualSpace
+ {
+ get { return _newCaretVirtualSpaces; }
+ }
+
+ /// <summary>
+ /// Move the caret and restore the selection as part of the Redo operation.
+ /// </summary>
+ protected override void DoMoveCaretAndSelect(ITextView view, IMapEditToData map)
+ {
+ SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex));
+ SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionAnchorIndex));
+ SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionActiveIndex));
+
+ view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _newCaretVirtualSpaces), (PositionAffinity)_newCaretAffinityByte);
+
+ view.Selection.Mode = _newSelectionMode;
+
+ var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _newSelectionAnchorVirtualSpaces);
+ var virtualActive = new VirtualSnapshotPoint(newActive, _newSelectionActiveVirtualSpaces);
+
+ // Buffer may have been changed by one of the listeners on the caret move event.
+ virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot);
+ virtualActive = virtualActive.TranslateTo(view.TextSnapshot);
+
+ view.Selection.Select(virtualAnchor, virtualActive);
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs
new file mode 100644
index 0000000..47abd9b
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs
@@ -0,0 +1,298 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the simpler
+ /// version of the primitive that handles most common cases.
+ /// </summary>
+ internal class BeforeTextBufferChangeUndoPrimitive : TextUndoPrimitive
+ {
+ // Think twice before adding any fields here! These objects are long-lived and consume considerable space.
+ // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below.
+ protected ITextUndoHistory _undoHistory;
+ protected int _oldCaretIndex;
+ protected byte _oldCaretAffinityByte;
+ protected bool _canUndo;
+
+ /// <summary>
+ /// Constructs a BeforeTextBufferChangeUndoPrimitive.
+ /// </summary>
+ /// <param name="textView">
+ /// The text view that was responsible for causing this change.
+ /// </param>
+ /// <param name="undoHistory">
+ /// The <see cref="ITextUndoHistory" /> this primitive will be added to.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="textView"/> is null.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception>
+ public static BeforeTextBufferChangeUndoPrimitive Create(ITextView textView, ITextUndoHistory undoHistory)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException("textView");
+ }
+ if (undoHistory == null)
+ {
+ throw new ArgumentNullException("undoHistory");
+ }
+
+ // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later.
+ if (!undoHistory.Properties.ContainsProperty(typeof(ITextView)))
+ {
+ undoHistory.Properties[typeof(ITextView)] = textView;
+ }
+
+ CaretPosition caret = textView.Caret.Position;
+
+ IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView);
+
+ int oldCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition);
+ int oldCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces;
+
+ VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint;
+ int oldSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position);
+ int oldSelectionAnchorVirtualSpaces = anchor.VirtualSpaces;
+
+ VirtualSnapshotPoint active = textView.Selection.ActivePoint;
+ int oldSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position);
+ int oldSelectionActiveVirtualSpaces = active.VirtualSpaces;
+
+ TextSelectionMode oldSelectionMode = textView.Selection.Mode;
+
+ if (oldCaretVirtualSpaces != 0 ||
+ oldSelectionAnchorIndex != oldCaretIndex ||
+ oldSelectionAnchorVirtualSpaces != 0 ||
+ oldSelectionActiveIndex != oldCaretIndex ||
+ oldSelectionActiveVirtualSpaces != 0 ||
+ oldSelectionMode != TextSelectionMode.Stream)
+ {
+ return new GeneralBeforeTextBufferChangeUndoPrimitive
+ (undoHistory, oldCaretIndex, caret.Affinity, oldCaretVirtualSpaces, oldSelectionAnchorIndex,
+ oldSelectionAnchorVirtualSpaces, oldSelectionActiveIndex, oldSelectionActiveVirtualSpaces, oldSelectionMode);
+ }
+ else
+ {
+ return new BeforeTextBufferChangeUndoPrimitive(undoHistory, oldCaretIndex, caret.Affinity);
+ }
+ }
+
+ //Get the map -- if any -- used to map points in the view's edit buffer to the data buffer. The map is needed because the undo history
+ //typically lives on the data buffer, but is used by the view on the edit buffer and a view (if any) on the data buffer. If there isn't
+ //a contract that guarantees that the contents of the edit and databuffers are the same, undoing an action on the edit buffer view and
+ //then undoing it on the data buffer view will cause cause the undo to try and restore caret/selection (in the data buffer) the coorinates
+ //saved in the edit buffer. This isn't good.
+ internal static IMapEditToData GetMap(ITextView view)
+ {
+ IMapEditToData map = null;
+ if (view.TextViewModel.EditBuffer != view.TextViewModel.DataBuffer)
+ {
+ view.Properties.TryGetProperty(typeof(IMapEditToData), out map);
+ }
+
+ return map;
+ }
+
+ //Map point from a position in the edit buffer to a position in the data buffer (== if there is no map, otherwise ask the map).
+ internal static int MapToData(IMapEditToData map, int point)
+ {
+ return (map != null) ? map.MapEditToData(point) : point;
+ }
+
+ //Map point from a position in the data buffer to a position in the edit buffer (== if there is no map, otherwise ask the map).
+ internal static int MapToEdit(IMapEditToData map, int point)
+ {
+ return (map != null) ? map.MapDataToEdit(point) : point;
+ }
+
+ protected BeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity)
+ {
+ _undoHistory = undoHistory;
+ _oldCaretIndex = caretIndex;
+ _oldCaretAffinityByte = (byte)caretAffinity;
+ _canUndo = true;
+ }
+
+ // Internal empty constructor for unit testing.
+ internal BeforeTextBufferChangeUndoPrimitive() { }
+
+ /// <summary>
+ /// The <see cref="ITextView"/> that this <see cref="BeforeTextBufferChangeUndoPrimitive"/> is bound to.
+ /// </summary>
+ internal ITextView GetTextView()
+ {
+ ITextView view = null;
+ _undoHistory.Properties.TryGetProperty(typeof(ITextView), out view);
+ return view;
+ }
+
+ #region UndoPrimitive Members
+
+ /// <summary>
+ /// Returns true if operation can be undone, false otherwise.
+ /// </summary>
+ public override bool CanUndo
+ {
+ get { return _canUndo; }
+ }
+
+ /// <summary>
+ /// Returns true if operation can be redone, false otherwise.
+ /// </summary>
+ public override bool CanRedo
+ {
+ get { return !_canUndo; }
+ }
+
+ /// <summary>
+ /// Do the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception>
+ public override void Do()
+ {
+ // Validate, we shouldn't be allowed to undo
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException(Strings.CannotRedo);
+ }
+
+ // Currently, no action is done on the redo. To set the caret and selection for after a TextBuffer change redo, there is the AfterTextBufferChangeUndoPrimitive.
+ // This Redo should not do anything with the caret and selection, because we only want to reset them after the TextBuffer change has occurred.
+ // Therefore, we need to add this UndoPrimitive to the undo stack before the UndoPrimitive for the TextBuffer change. On an undo, the TextBuffer changed UndoPrimitive
+ // will fire it's Undo first, and than the Undo for this UndoPrimitive will fire.
+ // However, on a redo, the Redo for this UndoPrimitive will be fired, and then the Redo for the TextBuffer change UndoPrimitive. If we had set any caret placement/selection here (ie the new caret placement/selection),
+ // we may crash because the TextBuffer change has not occurred yet (ie you try to set the caret to be at CharacterIndex 1 when the TextBuffer is still empty).
+
+ _canUndo = true;
+ }
+
+ /// <summary>
+ /// Undo the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Undo()
+ {
+ // Validate that we can undo this change
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException(Strings.CannotUndo);
+ }
+
+ // Restore the old caret position and active selection
+ var view = this.GetTextView();
+ Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen.");
+ if (view != null && !view.IsClosed)
+ {
+ UndoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view));
+ view.Caret.EnsureVisible();
+ }
+
+ _canUndo = false;
+ }
+
+ /// <summary>
+ /// Move the caret and restore the selection as part of the Undo operation.
+ /// </summary>
+ protected virtual void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map)
+ {
+ SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, MapToEdit(map, _oldCaretIndex));
+
+ view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret), (PositionAffinity)_oldCaretAffinityByte);
+ view.Selection.Clear();
+ }
+
+ protected virtual int OldCaretVirtualSpaces
+ {
+ get { return 0; }
+ }
+
+ public override bool CanMerge(ITextUndoPrimitive older)
+ {
+ if (older == null)
+ {
+ throw new ArgumentNullException("older");
+ }
+
+ AfterTextBufferChangeUndoPrimitive olderPrimitive = older as AfterTextBufferChangeUndoPrimitive;
+ // We can only merge with IUndoPrimitives of AfterTextBufferChangeUndoPrimitive type
+ if (olderPrimitive == null)
+ {
+ return false;
+ }
+
+ return (olderPrimitive.CaretIndex == _oldCaretIndex) && (olderPrimitive.CaretVirtualSpace == OldCaretVirtualSpaces);
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general
+ /// version of the primitive that handles all cases, including those involving selections and virtual space.
+ /// </summary>
+ internal class GeneralBeforeTextBufferChangeUndoPrimitive : BeforeTextBufferChangeUndoPrimitive
+ {
+ private int _oldCaretVirtualSpaces;
+ private int _oldSelectionAnchorIndex;
+ private int _oldSelectionAnchorVirtualSpaces;
+ private int _oldSelectionActiveIndex;
+ private int _oldSelectionActiveVirtualSpaces;
+ private TextSelectionMode _oldSelectionMode;
+
+ public GeneralBeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory,
+ int oldCaretIndex,
+ PositionAffinity oldCaretAffinity,
+ int oldCaretVirtualSpaces,
+ int oldSelectionAnchorIndex,
+ int oldSelectionAnchorVirtualSpaces,
+ int oldSelectionActiveIndex,
+ int oldSelectionActiveVirtualSpaces,
+ TextSelectionMode oldSelectionMode)
+ : base(undoHistory, oldCaretIndex, oldCaretAffinity)
+ {
+ _oldCaretVirtualSpaces = oldCaretVirtualSpaces;
+ _oldSelectionAnchorIndex = oldSelectionAnchorIndex;
+ _oldSelectionAnchorVirtualSpaces = oldSelectionAnchorVirtualSpaces;
+ _oldSelectionActiveIndex = oldSelectionActiveIndex;
+ _oldSelectionActiveVirtualSpaces = oldSelectionActiveVirtualSpaces;
+ _oldSelectionMode = oldSelectionMode;
+ }
+
+ /// <summary>
+ /// Move the caret and restore the selection as part of the Undo operation.
+ /// </summary>
+ protected override void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map)
+ {
+ SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldCaretIndex));
+ SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionAnchorIndex));
+ SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionActiveIndex));
+
+ view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _oldCaretVirtualSpaces), (PositionAffinity)_oldCaretAffinityByte);
+
+ view.Selection.Mode = _oldSelectionMode;
+
+ var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _oldSelectionAnchorVirtualSpaces);
+ var virtualActive = new VirtualSnapshotPoint(newActive, _oldSelectionActiveVirtualSpaces);
+
+ // Buffer may have been changed by one of the listeners on the caret move event.
+ virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot);
+ virtualActive = virtualActive.TranslateTo(view.TextSnapshot);
+
+ view.Selection.Select(virtualAnchor, virtualActive);
+ }
+
+ protected override int OldCaretVirtualSpaces
+ {
+ get { return _oldCaretVirtualSpaces; }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs
new file mode 100644
index 0000000..44fa09e
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs
@@ -0,0 +1,235 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Diagnostics;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Outlining;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+ /// <summary>
+ /// BeforeCollapsedMoveUndoPrimitive is added to the Undo stack before a collapsed region is moved.
+ /// Undo operations will cause this primitive to collapse the given regions, returning them
+ /// to their original pre-move state. Redo is ignored here and handled in the AfterCollapsedMoveUndoPrimitive.
+ /// </summary>
+ internal class BeforeCollapsedMoveUndoPrimitive : CollapsedMoveUndoPrimitive
+ {
+ public BeforeCollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collapsedSpans)
+ : base(outliningManager, textView, collapsedSpans)
+ {
+
+ }
+
+ /// <summary>
+ /// Redo the action
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Do()
+ {
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException(Strings.CannotRedo);
+ }
+
+ // Redo is handled by AfterCollapsedMoveUndoPrimitive and there is nothing for the before to Do here.
+ _canUndo = true;
+ }
+
+ /// <summary>
+ /// Undo the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Undo()
+ {
+ // Validate that we can undo this change
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException(Strings.CannotUndo);
+ }
+
+ CollapseRegions();
+
+ _canUndo = false;
+ }
+ }
+
+
+ /// <summary>
+ /// AfterCollapsedMoveUndoPrimitive is added to the Undo stack after a collapsed region has been moved.
+ /// When a Redo occurs this primitive will collapse the regions to return them to their correct state.
+ /// Undo operations are ignored here as they are handled in the BeforeCollapsedMoveUndoPrimitive.
+ /// </summary>
+ internal class AfterCollapsedMoveUndoPrimitive : CollapsedMoveUndoPrimitive
+ {
+ public AfterCollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collapsedSpans)
+ : base(outliningManager, textView, collapsedSpans)
+ {
+
+ }
+
+ /// <summary>
+ /// Redo the action
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Do()
+ {
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException(Strings.CannotRedo);
+ }
+
+ CollapseRegions();
+
+ _canUndo = true;
+ }
+
+ /// <summary>
+ /// Undo the action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Undo()
+ {
+ // Validate that we can undo this change
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException(Strings.CannotUndo);
+ }
+
+ // Undo is handled by the BeforeCollapsedMoveUndoPrimitive and so there is nothing to do here.
+ _canUndo = false;
+ }
+ }
+
+ /// <summary>
+ /// CollapsedMoveUndoPrimitive handles re-tagging of regions and collapsing them during moves.
+ /// OutliningUndoManager does not handle collapsing moved regions due to the way it listens to collapse/expands
+ /// events and then records an Expand undo operation for the collapse. For Move Line operations we need to always collapse.
+ /// Re-tagging of the outlined regions is needed since the tagging it not present on the newly inserted text
+ /// in the middle of the undo/redo operation, and without it the region cannot be collapsed.
+ /// </summary>
+ internal abstract class CollapsedMoveUndoPrimitive : TextUndoPrimitive
+ {
+ readonly IOutliningManager _outliningManager;
+ readonly ITextView _textView;
+ readonly IEnumerable<Tuple<Span, IOutliningRegionTag>> _collapsedSpans;
+ protected bool _canUndo;
+
+ public CollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collaspedSpans)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException("textView");
+ }
+
+ if (outliningManager == null)
+ {
+ throw new ArgumentNullException("outliningManager");
+ }
+
+ if (collaspedSpans == null)
+ {
+ throw new ArgumentNullException("collaspedSpans");
+ }
+
+ _outliningManager = outliningManager;
+ _textView = textView;
+ _collapsedSpans = collaspedSpans;
+
+ _canUndo = true;
+ }
+
+ // Re-collapse the spans
+ public void CollapseRegions()
+ {
+ if (_outliningManager == null || _textView == null || _collapsedSpans == null)
+ return;
+
+ ITextSnapshot snapshot = _textView.TextBuffer.CurrentSnapshot;
+
+ // Get a span that includes all collapsed regions
+ int min = Int32.MinValue;
+ int max = Int32.MinValue;
+
+ foreach (Tuple<Span, IOutliningRegionTag> span in _collapsedSpans)
+ {
+ if (min == Int32.MinValue || span.Item1.Start < min)
+ {
+ min = span.Item1.Start;
+ }
+
+ if (max == Int32.MinValue || span.Item1.End > max)
+ {
+ max = span.Item1.End;
+ }
+ }
+
+ // avoid running if there were no spans
+ if (min == Int32.MinValue)
+ {
+ Debug.Fail("No spans");
+ return;
+ }
+
+ // span containing all collapsed regions
+ SnapshotSpan entireSpan = new SnapshotSpan(snapshot, min, max - min);
+
+ // regions have not yet been tagged by the language service during the undo/redo and
+ // so we need to tag them again in order to do the collapse
+ SimpleTagger<IOutliningRegionTag> simpleTagger =
+ _textView.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(_textView.TextBuffer));
+
+ Debug.Assert(!simpleTagger.GetTaggedSpans(entireSpan).GetEnumerator().MoveNext(),
+ "The code is not expecting the regions to be tagged already. Verify that redundant tagging is not occurring.");
+
+ List<Span> toCollapse = new List<Span>();
+
+ // tag the regions and add them to the list to be bulk collapsed
+ foreach (Tuple<Span, IOutliningRegionTag> span in _collapsedSpans)
+ {
+ ITrackingSpan tspan = snapshot.CreateTrackingSpan(span.Item1, SpanTrackingMode.EdgeExclusive);
+ simpleTagger.CreateTagSpan(tspan, span.Item2);
+
+ toCollapse.Add(span.Item1);
+ }
+
+ // Disable the OutliningUndoManager to avoid it adding our collapse to the undo stack as an expand
+ bool disableOutliningUndo = _textView.Options.IsOutliningUndoEnabled();
+
+ try
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false);
+ }
+
+ // Do the collapse
+ _outliningManager.CollapseAll(entireSpan, colSpan => (!colSpan.IsCollapsed && toCollapse.Contains(colSpan.Extent.GetSpan(snapshot))));
+ }
+ finally
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true);
+ }
+ }
+ }
+
+ public override bool CanUndo
+ {
+ get { return _canUndo; }
+ }
+
+ public override bool CanRedo
+ {
+ get { return !_canUndo; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/EditorOperations/EditorOperations.cs b/src/Text/Impl/EditorOperations/EditorOperations.cs
new file mode 100644
index 0000000..0178e7a
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/EditorOperations.cs
@@ -0,0 +1,4590 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Threading;
+ using System.Windows;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Outlining;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Language.Intellisense.Utilities;
+
+ /// <summary>
+ /// Provides a default operations set on top of the text editor
+ /// </summary>
+ internal class EditorOperations : IEditorOperations3
+ {
+ enum CaretMovementDirection
+ {
+ Previous = 0,
+ Next = 1,
+ }
+
+ enum LetterCase
+ {
+ Uppercase = 0,
+ Lowercase = 1,
+ }
+
+ enum SelectionUpdate
+ {
+ Preserve,
+ Reset,
+ ResetUnlessEmptyBox,
+ Ignore,
+ ClearVirtualSpace
+ };
+
+ #region Private Members
+
+ ITextView _textView;
+ EditorOperationsFactoryService _factory;
+ ITextDocument _textDocument;
+ ITextStructureNavigator _textStructureNavigator;
+ ITextUndoHistory _undoHistory;
+ IViewPrimitives _editorPrimitives;
+ IEditorOptions _editorOptions;
+
+ private ITrackingSpan _immProvisionalComposition;
+
+ /// <summary>
+ /// A data format used to tag the contents of the clipboard so that it's clear
+ /// the data has been put in the clipboard by our editor
+ /// </summary>
+ private const string _clipboardLineBasedCutCopyTag = "VisualStudioEditorOperationsLineCutCopyClipboardTag";
+
+ /// <summary>
+ /// A data format used to tag the contents of the clipboard as a box selection.
+ /// This is the same string that was used in VS9 and previous versions.
+ /// </summary>
+ private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect";
+
+ #endregion // Private Members
+
+ /// <summary>
+ /// Constructs an <see cref="EditorOperations"/> bound to a given <see cref="ITextView"/>.
+ /// </summary>
+ /// <param name="textView">
+ /// The text editor to which this operations provider should bind to
+ /// </param>
+ internal EditorOperations(ITextView textView,
+ EditorOperationsFactoryService factory)
+ {
+ // Validate
+ if (textView == null)
+ throw new ArgumentNullException("textView");
+ if (factory == null)
+ throw new ArgumentNullException("factory");
+
+ _textView = textView;
+ _factory = factory;
+
+ _editorPrimitives = factory.EditorPrimitivesProvider.GetViewPrimitives(textView);
+ // Get the TextStructure Navigator
+ _textStructureNavigator = factory.TextStructureNavigatorFactory.GetTextStructureNavigator(_textView.TextBuffer);
+ Debug.Assert(_textStructureNavigator != null);
+
+ _undoHistory = factory.UndoHistoryRegistry.RegisterHistory(_textView.TextBuffer);
+ // Ensure that there is an ITextBufferUndoManager created for our TextBuffer
+ ITextBufferUndoManager textBufferUndoManager = factory.TextBufferUndoManagerProvider.GetTextBufferUndoManager(_textView.TextBuffer);
+ Debug.Assert(textBufferUndoManager != null);
+
+ _editorOptions = factory.EditorOptionsProvider.GetOptions(textView);
+
+ _factory.TextDocumentFactoryService.TryGetTextDocument(_textView.TextDataModel.DocumentBuffer, out _textDocument);
+ }
+
+
+ #region IEditorOperations2 Members
+
+ public bool MoveSelectedLinesUp()
+ {
+ Func<bool> action = () =>
+ {
+ bool success = false;
+
+ IWpfTextView view = _textView as IWpfTextView;
+
+ // find line start
+ IWpfTextViewLine startViewLine = GetLineStart(view, view.Selection.Start.Position);
+ SnapshotPoint start = startViewLine.Start;
+ ITextSnapshotLine startLine = start.GetContainingLine();
+
+ // find the last line view
+ IWpfTextViewLine endViewLine = GetLineEnd(view, view.Selection.End.Position);
+ SnapshotPoint end = endViewLine.EndIncludingLineBreak;
+ ITextSnapshotLine endLine = endViewLine.End.GetContainingLine();
+
+ ITextSnapshot snapshot = endLine.Snapshot;
+
+ // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line.
+ // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give
+ // a line within the collapsed region instead of skipping it all together.
+ if (GetLineEnd(view, startViewLine.Start) != endViewLine
+ && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start
+ && !view.Selection.End.IsInVirtualSpace)
+ {
+ endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1);
+ end = endLine.EndIncludingLineBreak;
+ endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1);
+ }
+
+ #region Initial Asserts
+
+ Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view.");
+
+ Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer.");
+
+ Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
+
+ #endregion
+
+ // check if we are at the top of the file, or trying to move a blank line
+ if (startLine.LineNumber < 1 || start == end)
+ {
+ // noop
+ success = true;
+ }
+ else
+ {
+ // find the line we are going to move over
+ ITextSnapshotLine prevLine = snapshot.GetLineFromLineNumber(startLine.LineNumber - 1);
+
+ // prevLineExtent is different from prevLine.Extent and avoids issues around collapsed regions
+ SnapshotPoint prevLineStart = GetLineStart(view, prevLine.Start).Start;
+ SnapshotSpan prevLineExtent = new SnapshotSpan(prevLineStart, prevLine.End);
+ SnapshotSpan prevLineExtentIncludingLineBreak = new SnapshotSpan(prevLineStart, prevLine.EndIncludingLineBreak);
+
+ using (ITextEdit edit = view.TextBuffer.CreateEdit())
+ {
+ int offset;
+
+ SnapshotSpan curLineExtent = new SnapshotSpan(startViewLine.Start, endViewLine.End);
+ SnapshotSpan curLineExtentIncLineBreak = new SnapshotSpan(startViewLine.Start, endViewLine.EndIncludingLineBreak);
+ string curLineText = curLineExtentIncLineBreak.GetText();
+
+ List<Tuple<Span, IOutliningRegionTag>> collapsedSpansInCurLine = null;
+ bool hasCollapsedRegions = false;
+
+ IOutliningManager outliningManager = (_factory.OutliningManagerService != null)
+ ? _factory.OutliningManagerService.GetOutliningManager(view)
+ : null;
+
+ if (outliningManager != null)
+ {
+ collapsedSpansInCurLine = outliningManager.GetCollapsedRegions(new NormalizedSnapshotSpanCollection(curLineExtent))
+ .Select(collapsed => Tuple.Create(collapsed.Extent.GetSpan(curLineExtent.Snapshot).Span, collapsed.Tag)).ToList();
+
+ hasCollapsedRegions = collapsedSpansInCurLine.Count > 0;
+
+ // check if we have collapsed spans in the selection and add the undo primitive if so
+ if (hasCollapsedRegions)
+ {
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp))
+ {
+ BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine);
+ undoTransaction.AddUndo(undoPrim);
+ undoTransaction.Complete();
+ }
+ }
+ }
+
+ string nextLineText = prevLineExtentIncludingLineBreak.GetText();
+
+ offset = nextLineText.Length;
+
+ // make the change
+ edit.Delete(curLineExtentIncLineBreak);
+ edit.Insert(prevLineExtentIncludingLineBreak.Start, curLineText);
+
+ // swap the line break around if needed for the last line of the selection
+ if (endViewLine.LineBreakLength == 0 && endViewLine.EndIncludingLineBreak == snapshot.Length)
+ {
+ // put the line break on the line we just moved that didn't have one
+ edit.Insert(prevLine.ExtentIncludingLineBreak.Start, prevLine.GetLineBreakText());
+
+ // delete the break from the line now at the end of the file
+ edit.Delete(new SnapshotSpan(prevLine.End, prevLine.EndIncludingLineBreak));
+ }
+
+
+ if (!edit.HasFailedChanges)
+ {
+ // store the position before the edit is applied
+ int anchorPos = view.Selection.AnchorPoint.Position.Position;
+ int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces;
+ int activePos = view.Selection.ActivePoint.Position.Position;
+ int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces;
+ var selectionMode = view.Selection.Mode;
+
+ // apply the edit
+ ITextSnapshot newSnapshot = edit.Apply();
+
+ if (newSnapshot != snapshot)
+ {
+
+ // Update the selection and caret position after the move
+ ITextSnapshot currentSnapshot = snapshot.TextBuffer.CurrentSnapshot;
+ VirtualSnapshotPoint desiredAnchor = new VirtualSnapshotPoint(
+ new SnapshotPoint(newSnapshot, Math.Min(anchorPos - offset, newSnapshot.Length)), anchorVirtualSpace)
+ .TranslateTo(currentSnapshot, PointTrackingMode.Negative);
+ VirtualSnapshotPoint desiredActive = new VirtualSnapshotPoint(
+ new SnapshotPoint(newSnapshot, Math.Min(activePos - offset, newSnapshot.Length)), activeVirtualSpace)
+ .TranslateTo(currentSnapshot, PointTrackingMode.Negative);
+
+ // Keep the selection and caret position the same
+ SelectAndMoveCaret(desiredAnchor, desiredActive, selectionMode, EnsureSpanVisibleOptions.None);
+
+ // Recollapse the spans
+ if (outliningManager != null && hasCollapsedRegions)
+ {
+ // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS
+ SimpleTagger<IOutliningRegionTag> simpleTagger =
+ view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(
+ () => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer));
+
+ if (simpleTagger != null)
+ {
+ if (hasCollapsedRegions)
+ {
+ List<Tuple<ITrackingSpan, IOutliningRegionTag>> addedSpans = collapsedSpansInCurLine.Select(tuple => Tuple.Create(newSnapshot.CreateTrackingSpan(tuple.Item1.Start - offset, tuple.Item1.Length,
+ SpanTrackingMode.EdgeExclusive), tuple.Item2)).ToList();
+
+ if (addedSpans.Count > 0)
+ {
+ List<Tuple<Span, IOutliningRegionTag>> spansForUndo = new List<Tuple<Span, IOutliningRegionTag>>();
+
+ foreach (var addedSpan in addedSpans)
+ {
+ simpleTagger.CreateTagSpan(addedSpan.Item1, addedSpan.Item2);
+ spansForUndo.Add(new Tuple<Span, IOutliningRegionTag>(addedSpan.Item1.GetSpan(newSnapshot), addedSpan.Item2));
+ }
+
+ SnapshotSpan changedSpan = new SnapshotSpan(addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).Start).Min(),
+ addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).End).Max());
+
+ List<SnapshotSpan> addedSnapshotSpans = addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot)).ToList();
+
+
+ bool disableOutliningUndo = _editorOptions.IsOutliningUndoEnabled();
+
+ // Recollapse the spans
+ // We need to disable the OutliningUndoManager for this operation otherwise an undo will expand it
+ try
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false);
+ }
+
+ outliningManager.CollapseAll(changedSpan, collapsible => addedSnapshotSpans.Contains(collapsible.Extent.GetSpan(newSnapshot)));
+ }
+ finally
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true);
+ }
+ }
+
+ // we need to recollapse after a redo
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp))
+ {
+ AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo);
+ undoTransaction.AddUndo(undoPrim);
+ undoTransaction.Complete();
+ }
+ }
+ }
+ }
+ }
+
+ success = true;
+ }
+ }
+ }
+ }
+
+ return success;
+ };
+
+ return ExecuteAction(Strings.MoveSelLinesUp, action, SelectionUpdate.Ignore, true);
+ }
+
+ public bool MoveSelectedLinesDown()
+ {
+ Func<bool> action = () =>
+ {
+
+ bool success = false;
+
+ IWpfTextView view = _textView as IWpfTextView;
+
+ // find line start
+ IWpfTextViewLine startViewLine = GetLineStart(view, view.Selection.Start.Position);
+ SnapshotPoint start = startViewLine.Start;
+ ITextSnapshotLine startLine = start.GetContainingLine();
+
+ // find the last line view
+ IWpfTextViewLine endViewLine = GetLineEnd(view, view.Selection.End.Position);
+ ITextSnapshotLine endLine = endViewLine.End.GetContainingLine();
+
+ ITextSnapshot snapshot = endLine.Snapshot;
+
+ // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line.
+ // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give
+ // a line within the collapsed region instead of skipping it all together.
+ if (GetLineEnd(view, startViewLine.Start) != endViewLine
+ && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start
+ && !view.Selection.End.IsInVirtualSpace)
+ {
+ endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1);
+ endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1);
+ }
+
+ #region Initial Asserts
+
+ Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view.");
+
+ Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer.");
+
+ Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
+
+ #endregion
+
+ // check if we are at the end of the file
+ if ((endLine.LineNumber + 1) >= snapshot.LineCount)
+ {
+ // noop
+ success = true;
+ }
+ else
+ {
+ // nextLineExtent is different from prevLine.Extent and avoids issues around collapsed regions
+ IWpfTextViewLine lastNextLine = GetLineEnd(view, endViewLine.EndIncludingLineBreak);
+ SnapshotSpan nextLineExtent = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.End);
+ SnapshotSpan nextLineExtentIncludingLineBreak = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.EndIncludingLineBreak);
+
+ using (ITextEdit edit = view.TextBuffer.CreateEdit())
+ {
+ SnapshotSpan curLineExtent = new SnapshotSpan(startViewLine.Start, endViewLine.End);
+ SnapshotSpan curLineExtentIncLineBreak = new SnapshotSpan(startViewLine.Start, endViewLine.EndIncludingLineBreak);
+ string curLineText = curLineExtentIncLineBreak.GetText();
+
+ string nextLineText = nextLineExtentIncludingLineBreak.GetText();
+
+ if (nextLineText.Length == 0)
+ {
+ // end of file - noop
+ success = true;
+ }
+ else
+ {
+ List<Tuple<Span, IOutliningRegionTag>> collapsedSpansInCurLine = null;
+ bool hasCollapsedRegions = false;
+
+ IOutliningManager outliningManager = (_factory.OutliningManagerService != null)
+ ? _factory.OutliningManagerService.GetOutliningManager(view)
+ : null;
+
+ if (outliningManager != null)
+ {
+ collapsedSpansInCurLine = outliningManager.GetCollapsedRegions(new NormalizedSnapshotSpanCollection(curLineExtent))
+ .Select(collapsed => Tuple.Create(collapsed.Extent.GetSpan(curLineExtent.Snapshot).Span, collapsed.Tag)).ToList();
+
+ hasCollapsedRegions = collapsedSpansInCurLine.Count > 0;
+
+ // check if we have collapsed spans in the selection and add the undo primitive if so
+ if (hasCollapsedRegions)
+ {
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown))
+ {
+ BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine);
+ undoTransaction.AddUndo(undoPrim);
+ undoTransaction.Complete();
+ }
+ }
+ }
+
+
+ int offset = nextLineText.Length;
+
+ // a line without a line break
+ if (nextLineExtent == nextLineExtentIncludingLineBreak)
+ {
+ string lineBreakText = new SnapshotSpan(startLine.End, startLine.EndIncludingLineBreak).GetText();
+
+ offset += lineBreakText.Length;
+
+ curLineText = lineBreakText + curLineText.Substring(0, curLineText.Length - lineBreakText.Length);
+ }
+
+
+ edit.Delete(curLineExtentIncLineBreak);
+ edit.Insert(nextLineExtentIncludingLineBreak.End, curLineText);
+
+ if (edit.HasFailedChanges)
+ {
+ success = false;
+ }
+ else
+ {
+ int anchorPos = view.Selection.AnchorPoint.Position.Position;
+ int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces;
+ int activePos = view.Selection.ActivePoint.Position.Position;
+ int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces;
+ var selectionMode = view.Selection.Mode;
+
+ ITextSnapshot newSnapshot = edit.Apply();
+ if (newSnapshot == snapshot)
+ {
+ success = false;
+ }
+ else
+ {
+ // Update the selection and caret position after the move
+ ITextSnapshot currentSnapshot = snapshot.TextBuffer.CurrentSnapshot;
+ VirtualSnapshotPoint desiredAnchor = new VirtualSnapshotPoint(new SnapshotPoint(newSnapshot, Math.Min(anchorPos + offset, newSnapshot.Length)),
+ anchorVirtualSpace).TranslateTo(currentSnapshot, PointTrackingMode.Negative);
+ VirtualSnapshotPoint desiredActive = new VirtualSnapshotPoint(new SnapshotPoint(newSnapshot, Math.Min(activePos + offset, newSnapshot.Length)),
+ activeVirtualSpace).TranslateTo(currentSnapshot, PointTrackingMode.Negative);
+
+ // keep the caret position and selection after the move
+ SelectAndMoveCaret(desiredAnchor, desiredActive, selectionMode, EnsureSpanVisibleOptions.None);
+
+ // Recollapse the spans
+ if (outliningManager != null && hasCollapsedRegions)
+ {
+ // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS
+ SimpleTagger<IOutliningRegionTag> simpleTagger =
+ view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer));
+
+ if (simpleTagger != null)
+ {
+ if (hasCollapsedRegions)
+ {
+ List<Tuple<ITrackingSpan, IOutliningRegionTag>> addedSpans = collapsedSpansInCurLine.Select(tuple => Tuple.Create(newSnapshot.CreateTrackingSpan(tuple.Item1.Start + offset,
+ tuple.Item1.Length, SpanTrackingMode.EdgeExclusive), tuple.Item2)).ToList();
+
+ if (addedSpans.Count > 0)
+ {
+ List<Tuple<Span, IOutliningRegionTag>> spansForUndo = new List<Tuple<Span, IOutliningRegionTag>>();
+
+ // add spans to tracking
+ foreach (var addedSpan in addedSpans)
+ {
+ simpleTagger.CreateTagSpan(addedSpan.Item1, addedSpan.Item2);
+ spansForUndo.Add(new Tuple<Span, IOutliningRegionTag>(addedSpan.Item1.GetSpan(newSnapshot), addedSpan.Item2));
+ }
+
+ SnapshotSpan changedSpan = new SnapshotSpan(addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).Start).Min(),
+ addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).End).Max());
+
+ List<SnapshotSpan> addedSnapshotSpans = addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot)).ToList();
+
+ bool disableOutliningUndo = _editorOptions.IsOutliningUndoEnabled();
+
+ // Recollapse the span
+ // We need to disable the OutliningUndoManager for this operation otherwise an undo will expand it
+ try
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false);
+ }
+
+ outliningManager.CollapseAll(changedSpan, collapsible => addedSnapshotSpans.Contains(collapsible.Extent.GetSpan(newSnapshot)));
+ }
+ finally
+ {
+ if (disableOutliningUndo)
+ {
+ _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true);
+ }
+ }
+
+ // we need to recollapse after a redo
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown))
+ {
+ AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo);
+ undoTransaction.AddUndo(undoPrim);
+ undoTransaction.Complete();
+ }
+ }
+ }
+ }
+ }
+
+ success = true;
+ }
+ }
+ }
+ }
+ }
+ return success;
+ };
+
+ return ExecuteAction(Strings.MoveSelLinesDown, action, SelectionUpdate.Ignore, true);
+ }
+
+ private static IWpfTextViewLine GetLineStart(IWpfTextView view, SnapshotPoint snapshotPoint)
+ {
+ IWpfTextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint);
+ while (!line.IsFirstTextViewLineForSnapshotLine)
+ {
+ line = view.GetTextViewLineContainingBufferPosition(line.Start - 1);
+ }
+ return line;
+ }
+
+ private static IWpfTextViewLine GetLineEnd(IWpfTextView view, SnapshotPoint snapshotPoint)
+ {
+ IWpfTextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint);
+ while (!line.IsLastTextViewLineForSnapshotLine)
+ {
+ line = view.GetTextViewLineContainingBufferPosition(line.EndIncludingLineBreak);
+ }
+ return line;
+ }
+
+ #endregion
+
+
+ #region IEditorOperations Members
+
+ public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint)
+ {
+ SelectAndMoveCaret(anchorPoint, activePoint, TextSelectionMode.Stream, EnsureSpanVisibleOptions.MinimumScroll);
+ }
+
+ public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, TextSelectionMode selectionMode)
+ {
+ this.SelectAndMoveCaret(anchorPoint, activePoint, selectionMode, EnsureSpanVisibleOptions.MinimumScroll);
+ }
+
+ public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, TextSelectionMode selectionMode, EnsureSpanVisibleOptions? scrollOptions)
+ {
+ bool empty = (anchorPoint == activePoint);
+
+ // TODO: Whenever caret/selection is updated to offer a way to set both simultaneously without either eventing before
+ // the other is updated, we should update this method to use that. There are potential bugs below in how clients
+ // react to things like selection moving. For example, if someone reacts to moving the selection by moving the caret,
+ // the logic below will override that caret position, which may not be desirable.
+
+ // The order of operations here is important:
+ // 1) We need to move the selection first. Clients (like VB) who listen for caret change need the selection to be correct,
+ // and we have yet to have clients that require the opposite order. See Dev10 #793198 for what happens when we do this selection-first.
+ //
+ // 2) Then we move the caret. This behaves differently, depending on if the new selection is empty or not (explained below).
+
+ if (empty)
+ {
+ _textView.Selection.Clear();
+ _textView.Selection.Mode = selectionMode;
+
+ // Since the selection is empty, move the caret to the provided active point and translate that point
+ // to the view's text snapshot (in case someone was listening to the selection changed event and made a text edit).
+ // The empty selection will track the caret.
+ // See Dev10 #785792 for an example of what happens when we get this wrong by moving the caret to the active point
+ // of the selection when the selection is being cleared.
+ _textView.Caret.MoveTo(activePoint.TranslateTo(_textView.TextSnapshot));
+ }
+ else
+ {
+ _textView.Selection.Select(anchorPoint, activePoint);
+ _textView.Selection.Mode = selectionMode;
+
+ // Move the caret to the active point of the selection (don't use activePoint since someone -- on the selection changed event -- might have
+ // moved the selection).
+ // But if the selection is empty (it shouldn't be since anchorPoint != activePoint, but those points could be normalized to an empty span
+ // or someone could have moved it), move the caret to the requested activePoint.
+ _textView.Caret.MoveTo(_textView.Selection.IsEmpty
+ ? activePoint.TranslateTo(_textView.TextSnapshot)
+ : _textView.Selection.ActivePoint);
+ }
+
+ // 3) If scrollOptions were provided, we're going to try and make the span visible using the provided options.
+ if (scrollOptions.HasValue)
+ {
+ //Make sure scrollOptions forces EnsureSpanVisible to bring the start or end of the selection into view appropriately.
+ if (_textView.Selection.IsReversed)
+ {
+ scrollOptions = scrollOptions.Value | EnsureSpanVisibleOptions.ShowStart;
+ }
+ else
+ {
+ scrollOptions = scrollOptions.Value & (~EnsureSpanVisibleOptions.ShowStart);
+ }
+
+ // Try to make the span visible. Since we are setting the scrollOptions above, this will ensure the caret
+ // is visible as well (we do not need to worry about the case where the caret is at the end of a word-wrapped
+ // line since -- when the caret is moved to a VirtualSnapshotPoint -- it won't be).
+ _textView.ViewScroller.EnsureSpanVisible(_textView.Selection.StreamSelectionSpan, scrollOptions.Value);
+ }
+ }
+
+ /// <summary>
+ /// Moves one character to the right.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveToNextCharacter(bool select)
+ {
+ _editorPrimitives.Caret.MoveToNextCharacter(select);
+ }
+
+ /// <summary>
+ /// Moves one character to the left.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveToPreviousCharacter(bool select)
+ {
+ bool isCaretAtStartOfViewLine = (!_textView.Caret.InVirtualSpace) &&
+ (_textView.Caret.Position.BufferPosition == _textView.Caret.ContainingTextViewLine.Start);
+
+ //Prevent the caret from moving from column 0 to the end of the previous line if either:
+ // virtual space is turned on or
+ // the user is extending a box selection.
+ if (isCaretAtStartOfViewLine && (_editorOptions.IsVirtualSpaceEnabled() || (select && (_textView.Selection.Mode == TextSelectionMode.Box))))
+ {
+ return;
+ }
+
+ _editorPrimitives.Caret.MoveToPreviousCharacter(select);
+ }
+
+ /// <summary>
+ /// Moves the caret to the next word.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether or not selection is extended as the caret is moved.
+ /// </param>
+ public void MoveToNextWord(bool select)
+ {
+ _editorPrimitives.Caret.MoveToNextWord(select);
+ }
+
+ /// <summary>
+ /// Moves the caret to the previous word.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether or not selection is extended as the caret is moved.
+ /// </param>
+ public void MoveToPreviousWord(bool select)
+ {
+ // In extending a box selection, we don't want this to jump to the previous line (if
+ // we are on the beginning of a line)
+ if (select && _textView.Selection.Mode == TextSelectionMode.Box && !_textView.Caret.InVirtualSpace)
+ {
+ if (_editorPrimitives.Caret.CurrentPosition == _editorPrimitives.Caret.StartOfViewLine)
+ return;
+ }
+
+ _editorPrimitives.Caret.MoveToPreviousWord(select);
+ }
+
+ /// <summary>
+ /// Sets the caret at the start of the document.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveToStartOfDocument(bool select)
+ {
+ _editorPrimitives.Caret.MoveToStartOfDocument(select);
+ }
+
+ /// <summary>
+ /// Sets the caret at the end of the document.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveToEndOfDocument(bool select)
+ {
+ _editorPrimitives.Caret.MoveToEndOfDocument(select);
+ }
+
+ /// <summary>
+ /// Moves the current line to the top of the view.
+ /// </summary>
+ public void MoveCurrentLineToTop()
+ {
+ _editorPrimitives.View.MoveLineToTop(_editorPrimitives.Caret.LineNumber);
+ }
+
+ /// <summary>
+ /// Moves the current line to the bottom of the view.
+ /// </summary>
+ public void MoveCurrentLineToBottom()
+ {
+ _editorPrimitives.View.MoveLineToBottom(_editorPrimitives.Caret.LineNumber);
+ }
+
+ public void MoveToStartOfLineAfterWhiteSpace(bool select)
+ {
+ int firstTextColumn = _editorPrimitives.Caret.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition;
+ if (firstTextColumn == _editorPrimitives.Caret.EndOfViewLine)
+ firstTextColumn = _editorPrimitives.Caret.StartOfViewLine;
+
+ _editorPrimitives.Caret.MoveTo(firstTextColumn, select);
+ }
+
+ public void MoveToStartOfNextLineAfterWhiteSpace(bool select)
+ {
+ DisplayTextPoint caretPoint = _editorPrimitives.Caret.Clone();
+ caretPoint.MoveToBeginningOfNextLine();
+
+ int firstTextColumn = caretPoint.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition;
+ if (firstTextColumn == caretPoint.EndOfViewLine)
+ firstTextColumn = caretPoint.StartOfViewLine;
+
+ _editorPrimitives.Caret.MoveTo(firstTextColumn, select);
+ }
+
+ public void MoveToStartOfPreviousLineAfterWhiteSpace(bool select)
+ {
+ DisplayTextPoint caretPoint = _editorPrimitives.Caret.Clone();
+ caretPoint.MoveToBeginningOfPreviousLine();
+
+ int firstTextColumn = caretPoint.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition;
+ if (firstTextColumn == caretPoint.EndOfViewLine)
+ firstTextColumn = caretPoint.StartOfViewLine;
+
+ _editorPrimitives.Caret.MoveTo(firstTextColumn, select);
+ }
+
+ public void MoveToLastNonWhiteSpaceCharacter(bool select)
+ {
+ int lastNonWhiteSpaceCharacterInLine = _editorPrimitives.Caret.EndOfViewLine - 1;
+ for (; lastNonWhiteSpaceCharacterInLine >= _editorPrimitives.Caret.StartOfViewLine; lastNonWhiteSpaceCharacterInLine--)
+ {
+ string nextCharacter = _editorPrimitives.View.GetTextPoint(lastNonWhiteSpaceCharacterInLine).GetNextCharacter();
+ if (!char.IsWhiteSpace(nextCharacter[0]))
+ {
+ break;
+ }
+ }
+
+ lastNonWhiteSpaceCharacterInLine = Math.Max(lastNonWhiteSpaceCharacterInLine, _editorPrimitives.Caret.StartOfLine);
+
+ if (lastNonWhiteSpaceCharacterInLine != _editorPrimitives.Caret.CurrentPosition)
+ {
+ _editorPrimitives.Caret.MoveTo(lastNonWhiteSpaceCharacterInLine, select);
+ }
+ }
+
+ public void MoveToTopOfView(bool select)
+ {
+ // TextViewLines may have both a partially and a fully hidden line at the top, or either, or neither.
+ ITextViewLineCollection lines = _editorPrimitives.View.AdvancedTextView.TextViewLines;
+ ITextViewLine firstVisibleLine = lines.FirstVisibleLine;
+ int iFirstVisibleLine = lines.GetIndexOfTextLine(firstVisibleLine);
+ ITextViewLine fullyVisibleLine = FindFullyVisibleLine(firstVisibleLine, iFirstVisibleLine + 1);
+
+ MoveCaretToTextLine(fullyVisibleLine, select);
+ }
+
+ public void MoveToBottomOfView(bool select)
+ {
+ // TextViewLines may have both a partially and a fully hidden line at the end, or either, or neither.
+ ITextViewLineCollection lines = _editorPrimitives.View.AdvancedTextView.TextViewLines;
+ ITextViewLine lastVisibleLine = lines.LastVisibleLine;
+ int iLastVisibleLine = lines.GetIndexOfTextLine(lastVisibleLine);
+ ITextViewLine fullyVisibleLine = FindFullyVisibleLine(lastVisibleLine, iLastVisibleLine - 1);
+
+ MoveCaretToTextLine(fullyVisibleLine, select);
+ }
+
+ /// <summary>
+ /// Deletes the word to the right of the current caret position.
+ /// </summary>
+ public bool DeleteWordToRight()
+ {
+ Func<bool> action = () =>
+ {
+ TextPoint startPointOfDelete = _editorPrimitives.Caret.Clone();
+
+ TextRange nextWord = startPointOfDelete.GetNextWord();
+ TextPoint endPointOfDelete = nextWord.GetStartPoint();
+
+ // If this delete did not start at the end of the line
+ // then only delete to the end of the line.
+ int endOfLine = startPointOfDelete.EndOfLine;
+ if (startPointOfDelete.CurrentPosition != endOfLine)
+ {
+ // It is possible that the text structure navigator has returned
+ // a word that spans multiple lines. In that case, just delete to
+ // the end of the line to match VS9 behavior.
+ if (endPointOfDelete.CurrentPosition > endOfLine)
+ {
+ endPointOfDelete.MoveTo(endOfLine);
+ }
+ else if (startPointOfDelete.CurrentPosition >= endPointOfDelete.CurrentPosition)
+ {
+ //If the startPointOfDelete was on the last word of the line then endPointOfDelete might be the
+ //start of the word so we should, instead, delete to the end of the line (or, at least, that is
+ //what we think is happening).
+ endPointOfDelete.MoveTo(endOfLine);
+ }
+ }
+
+ return ExpandRangeToIncludeSelection(startPointOfDelete.GetTextRange(endPointOfDelete)).Delete();
+ };
+
+ return ExecuteAction(Strings.DeleteWordToRight, action);
+ }
+
+ /// <summary>
+ /// Deletes the word to the left of the current caret position.
+ /// </summary>
+ public bool DeleteWordToLeft()
+ {
+ Func<bool> action = () =>
+ {
+ TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord();
+ TextRange rangeToDelete = currentWord;
+ if (_editorPrimitives.Caret.CurrentPosition > currentWord.GetStartPoint().CurrentPosition)
+ {
+ rangeToDelete = currentWord.GetStartPoint().GetTextRange(_editorPrimitives.Caret);
+ }
+ else
+ {
+ TextRange previousWord = _editorPrimitives.Caret.GetPreviousWord();
+ rangeToDelete = previousWord.GetStartPoint().GetTextRange(_editorPrimitives.Caret);
+ }
+
+ return ExpandRangeToIncludeSelection(rangeToDelete).Delete();
+ };
+
+ return ExecuteAction(Strings.DeleteWordToLeft, action);
+ }
+
+ public bool DeleteToBeginningOfLine()
+ {
+ Func<bool> action = () =>
+ {
+ TextRange selectionRange = _editorPrimitives.Selection.Clone();
+
+ if (_editorPrimitives.Selection.IsReversed || _editorPrimitives.Selection.IsEmpty)
+ {
+ selectionRange.SetStart(_editorPrimitives.View.GetTextPoint(_editorPrimitives.Caret.StartOfLine));
+ }
+
+ return selectionRange.Delete();
+ };
+
+ return ExecuteAction(Strings.DeleteToBOL, action);
+ }
+
+ public bool DeleteToEndOfLine()
+ {
+ Func<bool> action = () =>
+ {
+ TextRange selectionRange = _editorPrimitives.Selection.Clone();
+
+ if (!_editorPrimitives.Selection.IsReversed || _editorPrimitives.Selection.IsEmpty)
+ {
+ selectionRange.SetEnd(_editorPrimitives.View.GetTextPoint(_editorPrimitives.Caret.EndOfViewLine));
+ }
+
+ return selectionRange.Delete();
+ };
+
+ return ExecuteAction(Strings.DeleteToEOL, action);
+ }
+
+ /// <summary>
+ /// Deletes a character to the left of the current caret.
+ /// </summary>
+ public bool Backspace()
+ {
+ bool emptyBox = IsEmptyBoxSelection();
+ NormalizedSnapshotSpanCollection boxDeletions = null;
+
+ // First, handle cases that don't require edits
+ if (_textView.Selection.IsEmpty)
+ {
+ if (_textView.Caret.InVirtualSpace)
+ {
+ this.MoveCaretToPreviousIndentStopInVirtualSpace();
+
+ _textView.Caret.EnsureVisible();
+ return true;
+ }
+ if (_textView.Caret.Position.BufferPosition.Position == 0)
+ {
+ return true;
+ }
+ }
+ // If the entire selection is in virtual space, clear it
+ else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace))
+ {
+ this.ResetVirtualSelection();
+ _textView.Caret.EnsureVisible();
+ return true;
+ }
+ else if (emptyBox) // empty box selection, make sure it is valid
+ {
+ List<SnapshotSpan> spans = new List<SnapshotSpan>();
+
+ foreach (var span in _textView.Selection.VirtualSelectedSpans.Where(s => !s.IsInVirtualSpace).Select(s => s.SnapshotSpan))
+ {
+ var line = span.Start.GetContainingLine();
+ if (span.Start > line.Start)
+ {
+ spans.Add(_textView.GetTextElementSpan(span.Start - 1));
+ }
+ }
+
+ // If there is nothing to delete, clear the selection
+ if (spans.Count == 0)
+ {
+ _textView.Caret.MoveTo(_textView.Selection.Start);
+ _textView.Selection.Clear();
+ _textView.Caret.EnsureVisible();
+ return true;
+ }
+
+ boxDeletions = new NormalizedSnapshotSpanCollection(spans);
+ }
+
+ // Now, handle cases that require edits
+ Func<bool> action = () =>
+ {
+ // 1. An empty selection mean backspace the caret
+ if (_textView.Selection.IsEmpty)
+ return _editorPrimitives.Caret.DeletePrevious();
+
+ // 2. If this is an empty box, we may need to capture the new active/anchor points, as points in virtual space
+ // won't track as we want them to through the edit.
+ VirtualSnapshotPoint? anchorPoint = null;
+ VirtualSnapshotPoint? activePoint = null;
+
+ if (emptyBox)
+ {
+ if (_textView.Selection.AnchorPoint.IsInVirtualSpace)
+ {
+ anchorPoint = new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position, _textView.Selection.AnchorPoint.VirtualSpaces - 1);
+ }
+ if (_textView.Selection.ActivePoint.IsInVirtualSpace)
+ {
+ activePoint = new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position, _textView.Selection.ActivePoint.VirtualSpaces - 1);
+ }
+ }
+
+ // 3. The selection is non-empty, so delete the selected spans (unless this is an empty box selection: An empty box selection means treat this as a backspace on each line)
+ NormalizedSnapshotSpanCollection deletion = boxDeletions ?? _textView.Selection.SelectedSpans;
+
+ int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces;
+
+ if (!DeleteHelper(deletion))
+ return false;
+
+ // 5. Now, fix up the start and end points if this is an empty box
+ if (emptyBox && (anchorPoint.HasValue || activePoint.HasValue))
+ {
+ VirtualSnapshotPoint newAnchor = (anchorPoint.HasValue) ? anchorPoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.AnchorPoint;
+ VirtualSnapshotPoint newActive = (activePoint.HasValue) ? activePoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.ActivePoint;
+
+ _textView.Caret.MoveTo(_textView.Selection.ActivePoint);
+ _textView.Selection.Select(newAnchor, newActive);
+ }
+ else if (_textView.Selection.Mode != TextSelectionMode.Box)
+ {
+ //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space).
+ //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted).
+ _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces));
+ _textView.Selection.Clear();
+ }
+
+ _textView.Caret.EnsureVisible();
+ return true;
+ };
+
+ return ExecuteAction(Strings.DeleteCharToLeft, action, SelectionUpdate.ResetUnlessEmptyBox, true);
+ }
+
+ private void ResetVirtualSelection()
+ {
+ //Move the caret to the same line as the active point, but at the left edge of the current selection.
+ VirtualSnapshotPoint start = _textView.Selection.Start;
+ ITextViewLine startLine = _textView.GetTextViewLineContainingBufferPosition(start.Position);
+
+ VirtualSnapshotPoint end = _textView.Selection.End;
+ ITextViewLine endLine = _textView.GetTextViewLineContainingBufferPosition(end.Position);
+
+ double leftEdge = Math.Min(startLine.GetExtendedCharacterBounds(start).Left, endLine.GetExtendedCharacterBounds(end).Left);
+
+ ITextViewLine activeLine = (_textView.Selection.IsReversed) ? startLine : endLine;
+ VirtualSnapshotPoint newCaret = activeLine.GetInsertionBufferPositionFromXCoordinate(leftEdge);
+
+ _textView.Caret.MoveTo(newCaret);
+ _textView.Selection.Clear();
+ }
+
+ public bool DeleteFullLine()
+ {
+ return ExecuteAction(Strings.DeleteLine, () => GetFullLines().Delete());
+ }
+
+ public bool Tabify()
+ {
+ return ConvertLeadingWhitespace(Strings.Tabify, convertTabsToSpaces: false);
+ }
+
+ public bool Untabify()
+ {
+ return ConvertLeadingWhitespace(Strings.Untabify, convertTabsToSpaces: true);
+ }
+
+ public bool ConvertSpacesToTabs()
+ {
+ return ExecuteAction(Strings.ConvertSpacesToTabs, delegate { return ConvertSpacesAndTabsHelper(true); });
+ }
+
+ public bool ConvertTabsToSpaces()
+ {
+ return ExecuteAction(Strings.ConvertTabsToSpaces, delegate { return ConvertSpacesAndTabsHelper(false); });
+ }
+
+ public bool NormalizeLineEndings(string replacement)
+ {
+ return ExecuteAction(Strings.NormalizeLineEndings, () => NormalizeLineEndingsHelper(replacement));
+ }
+
+ /// <summary>
+ /// Selects the current word.
+ /// </summary>
+ public void SelectCurrentWord()
+ {
+ TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord();
+ TextRange previousWord = _editorPrimitives.Caret.GetPreviousWord();
+ TextRange selection = _editorPrimitives.Selection.Clone();
+
+ if (!_editorPrimitives.Selection.IsEmpty)
+ {
+ if ((
+ (selection.GetStartPoint().CurrentPosition == currentWord.GetStartPoint().CurrentPosition) &&
+ (selection.GetEndPoint().CurrentPosition == currentWord.GetEndPoint().CurrentPosition)) ||
+ (selection.GetStartPoint().CurrentPosition == previousWord.GetStartPoint().CurrentPosition) &&
+ (selection.GetEndPoint().CurrentPosition == previousWord.GetEndPoint().CurrentPosition))
+ {
+ // If the selection is already correct, don't select again, but *do* ensure
+ // the existing span is visible, even though we aren't moving the selection.
+ _textView.ViewScroller.EnsureSpanVisible(_textView.Selection.StreamSelectionSpan.SnapshotSpan, EnsureSpanVisibleOptions.MinimumScroll);
+ return;
+ }
+ }
+
+ // If the current word is blank, use the previous word if it is on the same line.
+ if (currentWord.IsEmpty)
+ {
+ TextRange previous = currentWord.GetStartPoint().GetPreviousWord();
+ if (previous.GetStartPoint().LineNumber == currentWord.GetStartPoint().LineNumber)
+ currentWord = previous;
+ }
+ _editorPrimitives.Selection.SelectRange(currentWord);
+
+ }
+
+ /// <summary>
+ /// Selects the enclosing.
+ /// </summary>
+ public void SelectEnclosing()
+ {
+ SnapshotSpan selectionSpan;
+ if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box)
+ {
+ selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0);
+ }
+ else
+ {
+ // This ignores virtual space
+ selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ }
+
+ Span span = _textStructureNavigator.GetSpanOfEnclosing(selectionSpan);
+
+ TextRange enclosingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End);
+ _editorPrimitives.Selection.SelectRange(enclosingRange);
+ }
+
+ /// <summary>
+ /// Selects the first child.
+ /// </summary>
+ public void SelectFirstChild()
+ {
+ SnapshotSpan selectionSpan;
+ if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box)
+ {
+ selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0);
+ }
+ else
+ {
+ // This ignores virtual space
+ selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ }
+
+ Span span = _textStructureNavigator.GetSpanOfFirstChild(selectionSpan);
+
+ TextRange firstChildRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End);
+ _editorPrimitives.Selection.SelectRange(firstChildRange);
+ }
+
+ /// <summary>
+ /// Selects the next sibling.
+ /// </summary>
+ /// <param name="extendSelection">Specifies whether the selection is to be extended or a new selection is to be made.</param>
+ public void SelectNextSibling(bool extendSelection)
+ {
+ SnapshotSpan selectionSpan;
+ if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box)
+ {
+ selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0);
+ }
+ else
+ {
+ // This ignores virtual space
+ selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ }
+
+ Span span = _textStructureNavigator.GetSpanOfNextSibling(selectionSpan);
+
+ _textView.Selection.Clear();
+ if (!span.IsEmpty && extendSelection)
+ {
+ // extend the selection to the end of the next sibling
+ int start = (span.Start <= selectionSpan.Start) ? span.Start : selectionSpan.Start;
+ int end = (span.End <= selectionSpan.End) ? selectionSpan.End : span.End;
+ span = Span.FromBounds(start, end);
+ }
+
+ TextRange nextSiblingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End);
+ _editorPrimitives.Selection.SelectRange(nextSiblingRange);
+ }
+
+ /// <summary>
+ /// Selects the previous sibling.
+ /// </summary>
+ /// <param name="extendSelection">Specifies whether the selection is to be extended or a new selection is to be made.</param>
+ public void SelectPreviousSibling(bool extendSelection)
+ {
+ SnapshotSpan selectionSpan;
+ if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box)
+ {
+ selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0);
+ }
+ else
+ {
+ // This ignores virtual space
+ selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ }
+
+ Span span = _textStructureNavigator.GetSpanOfPreviousSibling(selectionSpan);
+
+ _textView.Selection.Clear();
+ if (!span.IsEmpty && extendSelection)
+ {
+ // extend the selection to the start of the previous sibling
+ int start = (span.Start <= selectionSpan.Start) ? span.Start : selectionSpan.Start;
+ int end = (span.End <= selectionSpan.End) ? selectionSpan.End : span.End;
+ span = new Span(start, end - start);
+ }
+
+ TextRange previousSiblingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End);
+ _editorPrimitives.Selection.SelectRange(previousSiblingRange);
+ }
+
+ /// <summary>
+ /// Selects all text.
+ /// </summary>
+ public void SelectAll()
+ {
+ _editorPrimitives.Selection.SelectAll();
+ }
+
+ /// <summary>
+ /// Extends the current selection span to the new selection end.
+ /// </summary>
+ /// <param name="newEnd">
+ /// The new character position to extend the selection to.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="newEnd"/> is less than 0.</exception>
+ public void ExtendSelection(int newEnd)
+ {
+ _editorPrimitives.Selection.ExtendSelection(_editorPrimitives.Buffer.GetTextPoint(newEnd));
+ }
+
+ /// <summary>
+ /// Moves the caret to the given <paramref name="textLine"/> at the given horizontal offset <paramref name="horizontalOffset"/>.
+ /// </summary>
+ /// <param name="textLine">The <see cref="ITextViewLine"/> on which to place the caret.</param>
+ /// <param name="horizontalOffset">The horizontal location in the given <paramref name="textLine"/> at which to move the caret.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="textLine"/> is null.</exception>
+ public void MoveCaret(ITextViewLine textLine, double horizontalOffset, bool extendSelection)
+ {
+ if (textLine == null)
+ {
+ throw new ArgumentNullException("textLine");
+ }
+
+ if (extendSelection)
+ {
+ VirtualSnapshotPoint anchor = _textView.Selection.AnchorPoint;
+ _textView.Caret.MoveTo(textLine, horizontalOffset);
+
+ // It is possible that the text was modified as part of this caret move so translate the anchor
+ _textView.Selection.Select(anchor.TranslateTo(_textView.TextSnapshot), _textView.Caret.Position.VirtualBufferPosition);
+ }
+ else
+ {
+ // Retain the selection mode, even though we are clearing it
+ bool inBox = _textView.Selection.Mode == TextSelectionMode.Box;
+
+ _textView.Selection.Clear();
+
+ if (inBox)
+ _textView.Selection.Mode = TextSelectionMode.Box;
+
+ _textView.Caret.MoveTo(textLine, horizontalOffset);
+ }
+ }
+
+ /// <summary>
+ /// Puts the caret one line up.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveLineUp(bool select)
+ {
+ _editorPrimitives.Caret.MoveToPreviousLine(select);
+ }
+
+ /// <summary>
+ /// Puts the caret one line down.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveLineDown(bool select)
+ {
+ _editorPrimitives.Caret.MoveToNextLine(select);
+ }
+
+ /// <summary>
+ /// Moves the caret one page up.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void PageUp(bool select)
+ {
+ _editorPrimitives.Caret.MovePageUp(select);
+ }
+
+ /// <summary>
+ /// Moves the caret one page down.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void PageDown(bool select)
+ {
+ _editorPrimitives.Caret.MovePageDown(select);
+ }
+
+ /// <summary>
+ /// Moves the caret to the end of the view line.
+ /// </summary>
+ /// <param name="select">
+ /// Specifies whether selection is made as the caret is moved.
+ /// </param>
+ public void MoveToEndOfLine(bool select)
+ {
+ // If the caret is at the start of an empty line, respond by trying to position
+ // the caret at the smart indent location.
+ if (_textView.Caret.Position.BufferPosition.GetContainingLine().Extent.IsEmpty &&
+ !_textView.Caret.InVirtualSpace)
+ {
+ if (PositionCaretWithSmartIndent(useOnlyVirtualSpace: true, extendSelection: select))
+ {
+ _editorPrimitives.Caret.EnsureVisible();
+ return;
+ }
+ }
+
+ _editorPrimitives.Caret.MoveToEndOfViewLine(select);
+ }
+
+ public void MoveToHome(bool select)
+ {
+ int newPosition = _editorPrimitives.Caret.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition;
+
+ // If the caret is already at the first non-whitespace character or
+ // the line is entirely whitepsace, move to the start of the view line.
+ if (newPosition == _editorPrimitives.Caret.CurrentPosition ||
+ newPosition == _editorPrimitives.Caret.EndOfViewLine)
+ {
+ newPosition = _editorPrimitives.Caret.StartOfViewLine;
+ }
+
+ _editorPrimitives.Caret.MoveTo(newPosition, select);
+ }
+
+ public void MoveToStartOfLine(bool select)
+ {
+ _editorPrimitives.Caret.MoveToStartOfViewLine(select);
+ }
+
+ /// <summary>
+ /// Inserts a new line at the current caret position.
+ /// </summary>
+ public bool InsertNewLine()
+ {
+ Func<bool> action = () =>
+ {
+ VirtualSnapshotPoint caret = _textView.Caret.Position.VirtualBufferPosition;
+ ITextSnapshotLine line = caret.Position.GetContainingLine();
+ ITextSnapshot snapshot = line.Snapshot;
+
+ // todo: the following logic is duplicated in DefaultTextPointPrimitive.InsertNewLine()
+ // didn't call that method here because it would result in two text transactions
+ // ultimately everything here should probably move into primitives.
+ string textToInsert = TextBufferOperationHelpers.GetNewLineCharacterToInsert(line, _editorOptions);
+
+ bool succeeded = false;
+ bool caretMoved = false;
+ EventHandler<CaretPositionChangedEventArgs> caretWatcher = delegate (object sender, CaretPositionChangedEventArgs e)
+ {
+ caretMoved = true;
+ };
+
+ // Indent unless the caret is at column 0 or the current line is empty.
+ // This appears to be added as a fix for Venus; which combined with our implementation of
+ // PositionCaretWithSmartIndent does not indent correctly on NewLine when Caret is at column 0.
+ bool doIndent = caret.IsInVirtualSpace || (caret.Position != _textView.Caret.ContainingTextViewLine.Start)
+ || (_textView.Caret.ContainingTextViewLine.Extent.Length == 0);
+
+ try
+ {
+ using (var edit = _textView.TextBuffer.CreateEdit())
+ {
+ _textView.Caret.PositionChanged += caretWatcher;
+ int searchIndexforPreviousWhitespaces = -1;
+ var lineContainingTrimTrailingWhitespacesSearchindex = line; // usually is the line containing caret.
+
+ if (_textView.Selection.Mode == TextSelectionMode.Stream)
+ {
+ // This ignores virtual space
+ Span selection = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ succeeded = edit.Replace(selection, textToInsert);
+ // For stream selection you should always look for trimming whitespaces previous to selection.start instead of caret position
+ lineContainingTrimTrailingWhitespacesSearchindex = snapshot.GetLineFromPosition(selection.Start);
+ searchIndexforPreviousWhitespaces = selection.Start - lineContainingTrimTrailingWhitespacesSearchindex.Start.Position;
+ }
+ else
+ {
+ var isDeleteSuccessfull = true;
+ searchIndexforPreviousWhitespaces = caret.Position.Position - line.Start.Position;
+ foreach (var span in _textView.Selection.SelectedSpans)
+ {
+ // In a box selection if the caret is forward positioned then
+ //we should search for whitespaces from the start of the last span since the spans are not yet deleted
+ if (span.End.Position == caret.Position.Position)
+ {
+ searchIndexforPreviousWhitespaces = span.Start.Position - line.Start.Position;
+ }
+ if (!edit.Delete(span))
+ {
+ isDeleteSuccessfull = false;
+ }
+ }
+ if (!isDeleteSuccessfull)
+ return false;
+ succeeded = edit.Replace(new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0),
+ textToInsert);
+ }
+ // Trim traling whitespaces as we insert the new line as well if the editor option is set
+ if (_editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId))
+ {
+ var previousNonWhitespaceCharacterIndex = lineContainingTrimTrailingWhitespacesSearchindex.IndexOfPreviousNonWhiteSpaceCharacter(searchIndexforPreviousWhitespaces);
+
+ // Note: If previousNonWhiteSpaceCharacter index is -1 this will automatically default to line.start.position
+ var startIndexForTrailingWhitespaceSpan = lineContainingTrimTrailingWhitespacesSearchindex.Start.Position + previousNonWhitespaceCharacterIndex + 1;
+ var lengthOfTrailingWhitespaceSpan = searchIndexforPreviousWhitespaces - previousNonWhitespaceCharacterIndex - 1;
+
+ if (lengthOfTrailingWhitespaceSpan != 0) // If there are any whitespaces before the caret delete them
+ edit.Delete(new Span(startIndexForTrailingWhitespaceSpan, lengthOfTrailingWhitespaceSpan));
+ }
+
+ // Apply all changes
+ succeeded = (edit.Apply() != snapshot);
+ }
+ }
+ finally
+ {
+ _textView.Caret.PositionChanged -= caretWatcher;
+ }
+
+ if (succeeded)
+ {
+ if (doIndent)
+ {
+ caret = _textView.Caret.Position.VirtualBufferPosition;
+ line = caret.Position.GetContainingLine();
+
+ //Only attempt to auto indent if -- after the edit above -- no one moved the caret on the buffer change
+ //and the caret is at the start of its new line (no one did any funny edits to the buffer on the buffer change).
+ if ((!caretMoved) && (caret.Position == line.Start))
+ {
+ caretMoved = PositionCaretWithSmartIndent(useOnlyVirtualSpace: false, extendSelection: false);
+ if (!caretMoved && caret.IsInVirtualSpace)
+ {
+ //No smart indent logic so make sure the caret is not in virtual space.
+ _textView.Caret.MoveTo(caret.Position);
+ }
+ }
+ }
+ ResetSelection();
+ }
+ return succeeded;
+ };
+ return ExecuteAction(Strings.InsertNewLine, action, SelectionUpdate.Ignore, true);
+ }
+
+
+
+ public bool OpenLineAbove()
+ {
+ Func<bool> action = () =>
+ {
+ bool result;
+ DisplayTextPoint textPoint = _editorPrimitives.Caret.Clone();
+ if (textPoint.LineNumber == 0)
+ {
+ _editorPrimitives.Caret.MoveTo(0);
+ result = this.InsertNewLine();
+ if (result)
+ {
+ // leave caret on the new line
+ _editorPrimitives.Caret.MoveTo(0);
+ }
+ }
+ else
+ {
+ textPoint.MoveToBeginningOfPreviousViewLine();
+ textPoint.MoveToEndOfViewLine();
+ _editorPrimitives.Caret.MoveTo(textPoint.CurrentPosition);
+ result = this.InsertNewLine();
+ }
+ return result;
+ };
+ return ExecuteAction(Strings.OpenLineAbove, action);
+ }
+
+ public bool OpenLineBelow()
+ {
+ Func<bool> action = () =>
+ {
+ _editorPrimitives.Caret.MoveToEndOfViewLine();
+ return this.InsertNewLine();
+ };
+ return ExecuteAction(Strings.OpenLineBelow, action);
+ }
+
+ /// <summary>
+ /// If there is a multi-line selection, indents the selection, otherwise inserts a tab at the caret location.
+ /// </summary>
+ public bool Indent()
+ {
+ bool insertTabs = _textView.Selection.Mode == TextSelectionMode.Box || !IndentOperationShouldBeMultiLine;
+
+ Func<bool> action = () =>
+ {
+ if (insertTabs)
+ {
+ return this.EditHelper(edit =>
+ {
+ int tabSize = _editorOptions.GetTabSize();
+ int indentSize = _editorOptions.GetIndentSize();
+ bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled();
+ bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
+ _textView.Selection.Start != _textView.Selection.End;
+
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.AnchorPoint, indentSize) : null;
+ VirtualSnapshotPoint? activePoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.ActivePoint, indentSize) : null;
+
+ // Insert an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (!InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false))
+ return false;
+ }
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
+ });
+ }
+ else
+ {
+ return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
+ }
+ };
+
+ return ExecuteAction(Strings.InsertTab, action, (_textView.Selection.IsEmpty) ? SelectionUpdate.ClearVirtualSpace : SelectionUpdate.Ignore, ensureVisible: insertTabs);
+ }
+
+ /// <summary>
+ /// If there is a multi-line selection, unindents the selection. If there is a single line selection,
+ /// removes up to a indent's worth of whitespace from before the start of the selection. If there is no selection,
+ /// removes up to a intent's worth of whitespace from before the caret position.
+ /// </summary>
+ public bool Unindent()
+ {
+ bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
+ _textView.Selection.Start != _textView.Selection.End;
+
+ Func<bool> action = null;
+
+ if (_textView.Caret.InVirtualSpace && _textView.Selection.IsEmpty)
+ {
+ this.MoveCaretToPreviousIndentStopInVirtualSpace();
+
+ return true;
+ }
+ else if (!boxSelection && IndentOperationShouldBeMultiLine)
+ {
+ action = () => PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint);
+ }
+ else if (!boxSelection)
+ {
+ action = () => EditHelper(edit => RemoveIndentAtPoint(_textView.Selection.Start.Position, edit, failOnNonWhitespaceCharacter: false));
+ }
+ else // Box selection
+ {
+ int columnsToRemove = DetermineMaxBoxUnindent();
+
+ action = () => EditHelper(edit =>
+ {
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
+ VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
+
+ // Remove an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove))
+ return false;
+ }
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
+
+ });
+ }
+
+ return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true);
+ }
+
+ public bool IncreaseLineIndent()
+ {
+ Func<bool> action = () =>
+ {
+ return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
+ };
+
+ return ExecuteAction(Strings.IncreaseLineIndent, action, SelectionUpdate.Ignore, ensureVisible: true);
+ }
+
+ public bool DecreaseLineIndent()
+ {
+ Func<bool> action = () =>
+ {
+ return PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint);
+ };
+
+ return ExecuteAction(Strings.DecreaseLineIndent, action, SelectionUpdate.Ignore, ensureVisible: true);
+ }
+
+ public bool DeleteBlankLines()
+ {
+ Func<bool> action = () =>
+ {
+ double oldLeft = _textView.Caret.Left;
+ using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit())
+ {
+ int startLine = _editorPrimitives.Selection.GetStartPoint().LineNumber;
+ int endLine = _editorPrimitives.Selection.GetEndPoint().LineNumber;
+
+ // If the selection is empty, we follow this algorithm:
+ // If the current line the caret is on is blank or the caret is at the end of the line
+ // delete all blank lines that occur later in the file.
+ // If the caret is not on a blank line and there are no blank lines below the caret
+ // then delete all blank lines between the caret line and the next non-blank line.
+ // This matches VS9 behavior.
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ // First search downwards to see if there are blank lines to delete if we are at the end of the
+ // line or if we are on a blank line.
+ TextPoint startOfLine = _editorPrimitives.Buffer.GetTextPoint(startLine, 0);
+
+ if (IsPointOnBlankLine(startOfLine) ||
+ (_editorPrimitives.Caret.CurrentPosition == _editorPrimitives.Caret.EndOfLine)) //Caret is at the physical end of a line
+ {
+ while (endLine < _editorPrimitives.Selection.AdvancedSelection.TextView.TextSnapshot.LineCount - 1)
+ {
+ TextPoint startOfNextLine = _editorPrimitives.Buffer.GetTextPoint(endLine + 1, 0);
+ if (!IsPointOnBlankLine(startOfNextLine))
+ {
+ break;
+ }
+ endLine++;
+ }
+ }
+
+ // If there are no blank lines below the current line and
+ // this is not a blank line, look up to see if there are any
+ // blank lines directly above the current one
+ if (startLine == endLine)
+ {
+ if (!IsPointOnBlankLine(startOfLine))
+ {
+ while (startLine > 0)
+ {
+ startOfLine = _editorPrimitives.Buffer.GetTextPoint(startLine - 1, 0);
+ if (!IsPointOnBlankLine(startOfLine))
+ {
+ break;
+ }
+ startLine--;
+ }
+ }
+ }
+ }
+
+ for (int i = startLine; i <= endLine; i++)
+ {
+ TextPoint startPoint = _editorPrimitives.Buffer.GetTextPoint(i, 0);
+
+ if (IsPointOnBlankLine(startPoint))
+ {
+ TextPoint startOfNextLine = startPoint.Clone();
+ startOfNextLine.MoveToBeginningOfNextLine();
+ if (!textEdit.Delete(Span.FromBounds(startPoint.CurrentPosition, startOfNextLine.CurrentPosition)))
+ return false;
+ }
+ }
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+
+ _textView.Caret.EnsureVisible();
+ ITextViewLine newLine = _textView.Caret.ContainingTextViewLine;
+ _textView.Caret.MoveTo(newLine, oldLeft);
+
+ return true;
+ }
+ };
+
+ return ExecuteAction(Strings.DeleteBlankLines, action);
+ }
+
+ public bool DeleteHorizontalWhiteSpace()
+ {
+ return ExecuteAction(Strings.DeleteHorizontalWhiteSpace, DeleteHorizontalWhitespace);
+ }
+
+ public Boolean InsertFinalNewLine()
+ {
+ Func<bool> action = () =>
+ {
+ var buffer = _textView.TextBuffer;
+ return TextBufferOperationHelpers.TryInsertFinalNewLine(buffer, _editorOptions);
+ };
+
+ return ExecuteAction(Strings.InsertFinalNewLine, action, SelectionUpdate.Ignore, true);
+ }
+
+ public bool TrimTrailingWhiteSpace()
+ {
+ Func<bool> action = () =>
+ {
+ ITextSnapshot snapshot = _textView.TextSnapshot;
+ var startLine = snapshot.GetLineFromPosition(_textView.Selection.Start.Position);
+ var endLine = snapshot.GetLineFromPosition(_textView.Selection.End.Position);
+
+ return TrimTrailingWhiteSpace(startLine, endLine);
+ };
+
+ return ExecuteAction(Strings.TrimTrailingWhitespace, action, SelectionUpdate.Ignore, true);
+ }
+
+ private bool TrimTrailingWhiteSpace(ITextSnapshotLine line)
+ {
+ return TrimTrailingWhiteSpace(line, line);
+ }
+
+ private bool TrimTrailingWhiteSpace(ITextSnapshotLine startLine, ITextSnapshotLine endLine)
+ {
+ Debug.Assert(startLine.LineNumber <= endLine.LineNumber);
+
+ IWpfTextView view = _textView as IWpfTextView;
+ bool isEditMade = false;
+ bool success = true;
+
+ using (ITextEdit edit = view.TextBuffer.CreateEdit())
+ {
+ var currentSnapshot = _textView.TextBuffer.CurrentSnapshot;
+ for (int i = startLine.LineNumber; i <= endLine.LineNumber; i++)
+ {
+ var currentTrailingSpan = GetTrailingWhitespaceSpanToDelete(currentSnapshot.GetLineFromLineNumber(i));
+ if (currentTrailingSpan != null && currentTrailingSpan.HasValue && !currentTrailingSpan.Value.IsEmpty)
+ {
+ isEditMade = true;
+ edit.Delete(currentTrailingSpan.Value);
+ }
+ }
+
+ if (isEditMade)
+ {
+ success = (edit.Apply() != currentSnapshot);
+ }
+ }
+
+ return success;
+ }
+
+ private Span? GetTrailingWhitespaceSpanToDelete(ITextSnapshotLine line)
+ {
+ int indexOfLastNonWhitespaceCharacter = -1;
+
+ for (int i = line.End.Position - 1; i >= line.Start.Position; i--)
+ {
+ if (!char.IsWhiteSpace(line.Snapshot[i]))
+ {
+ break;
+ }
+ indexOfLastNonWhitespaceCharacter = i;
+ }
+
+ if (indexOfLastNonWhitespaceCharacter != -1)
+ return new Span(indexOfLastNonWhitespaceCharacter, line.End.Position - indexOfLastNonWhitespaceCharacter);
+ return null;
+ }
+
+ /// <summary>
+ /// Inserts the given text at the current caret position.
+ /// </summary>
+ /// <param name="text">
+ /// The text to be inserted in the buffer.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public bool InsertText(string text)
+ {
+ return this.InsertText(text, true);
+ }
+
+ public bool InsertProvisionalText(string text)
+ {
+ return this.InsertText(text, false);
+ }
+
+ public ITrackingSpan ProvisionalCompositionSpan { get { return _immProvisionalComposition; } }
+
+ public IEditorOptions Options { get { return _editorOptions; } }
+
+ public string SelectedText
+ {
+ get
+ {
+ string text;
+ if (_textView.Selection.SelectedSpans.Count > 1)
+ {
+ text = string.Join(_editorOptions.GetNewLineCharacter(), _textView.Selection.SelectedSpans
+ .Select((span) => span.GetText())
+ .ToArray());
+
+ // Append one last newline character
+ text += _editorOptions.GetNewLineCharacter();
+ }
+ else
+ {
+ text = _textView.Selection.StreamSelectionSpan.GetText();
+ }
+
+ return text;
+ }
+ }
+
+ /// <summary>
+ /// Selects the given line.
+ /// </summary>
+ /// <param name="extendSelection">
+ /// Specifies whether the selection is to be extended or a new selection is to be made.
+ /// </param>
+ public void SelectLine(ITextViewLine viewLine, bool extendSelection)
+ {
+ if (viewLine == null)
+ throw new ArgumentNullException("viewLine");
+
+ SnapshotPoint anchor;
+ SnapshotPoint active;
+ if (!extendSelection || _textView.Selection.IsEmpty)
+ {
+ // This is always going to be a forward span
+ anchor = viewLine.Start;
+ active = viewLine.EndIncludingLineBreak;
+ }
+ else
+ {
+ ITextViewLine anchorLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.AnchorPoint.Position);
+ if (_textView.Selection.IsReversed && (!_textView.Selection.AnchorPoint.IsInVirtualSpace) &&
+ (_textView.Selection.AnchorPoint.Position == anchorLine.Start) &&
+ anchorLine.Start.Position > 0)
+ {
+ //In a reversed selection, an anchor that starts at the begining of a line really corresponds
+ //to the end of the previous line.
+ anchorLine = _textView.GetTextViewLineContainingBufferPosition(anchorLine.Start - 1);
+ }
+
+ if (viewLine.Start < anchorLine.Start)
+ {
+ //Grow the selection (creating a reversed selection)
+ anchor = anchorLine.EndIncludingLineBreak;
+ active = viewLine.Start;
+ }
+ else
+ {
+ anchor = anchorLine.Start;
+ active = viewLine.EndIncludingLineBreak;
+ }
+ }
+
+ //Only try and show the caret, not the entire selection.
+ this.SelectAndMoveCaret(new VirtualSnapshotPoint(anchor), new VirtualSnapshotPoint(active), TextSelectionMode.Stream, null);
+ _textView.Caret.EnsureVisible();
+ }
+
+ /// <summary>
+ /// Resets any selection in the text.
+ /// </summary>
+ public void ResetSelection()
+ {
+ _textView.Selection.Clear();
+ }
+
+ /// <summary>
+ /// Deletes the selection, if present, or the next character in the text buffer.
+ /// </summary>
+ public bool Delete()
+ {
+ bool emptyBox = IsEmptyBoxSelection();
+ NormalizedSnapshotSpanCollection boxDeletions = null;
+
+ // First, handle cases that don't require edits
+ if (_textView.Selection.IsEmpty)
+ {
+ if (_textView.Caret.Position.BufferPosition.Position == _textView.TextSnapshot.Length)
+ {
+ return true;
+ }
+ }
+ // If the entire selection is empty and in virtual space, clear it
+ else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace))
+ {
+ this.ResetVirtualSelection();
+ return true;
+ }
+ else if (emptyBox) // empty box selection, make sure it is valid
+ {
+ List<SnapshotSpan> spans = new List<SnapshotSpan>();
+
+ foreach (var span in _textView.Selection.SelectedSpans)
+ {
+ var line = span.Start.GetContainingLine();
+ if (span.Start < line.End)
+ {
+ spans.Add(_textView.GetTextElementSpan(span.Start));
+ }
+ }
+
+ // If there is nothing to delete, clear the selection
+ if (spans.Count == 0)
+ {
+ _textView.Caret.MoveTo(_textView.Selection.Start);
+ _textView.Selection.Clear();
+ return true;
+ }
+
+ boxDeletions = new NormalizedSnapshotSpanCollection(spans);
+ }
+
+
+ // Now handle cases that require edits
+ Func<bool> action = () =>
+ {
+ if (_textView.Selection.IsEmpty)
+ {
+ CaretPosition position = _textView.Caret.Position;
+ if (position.VirtualBufferPosition.IsInVirtualSpace)
+ {
+ string whitespace = GetWhitespaceForVirtualSpace(position.VirtualBufferPosition);
+ SnapshotSpan span = _textView.GetTextElementSpan(_textView.Caret.Position.VirtualBufferPosition.Position);
+
+ return ReplaceHelper(span, whitespace);
+ }
+ else
+ {
+ return DeleteHelper(_textView.GetTextElementSpan(position.VirtualBufferPosition.Position));
+ }
+ }
+ else
+ {
+ // The selection is non-empty, so delete selected spans
+ NormalizedSnapshotSpanCollection deletion = _textView.Selection.SelectedSpans;
+
+ // Unless it is an empty box selection, so treat it as a delete on each line
+ if (emptyBox && boxDeletions != null)
+ deletion = boxDeletions;
+
+ int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces;
+ bool succeeded = DeleteHelper(deletion);
+
+ if (succeeded && (_textView.Selection.Mode != TextSelectionMode.Box))
+ {
+ //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space).
+ //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted).
+ _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces));
+ _textView.Selection.Clear();
+ }
+
+ return succeeded;
+ }
+ };
+
+ return ExecuteAction(Strings.DeleteText, action, SelectionUpdate.ResetUnlessEmptyBox, true);
+ }
+
+ /// <summary>
+ /// Replace text selection with the new text.
+ /// </summary>
+ /// <param name="text">
+ /// The new text that will replace the old selection.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public bool ReplaceSelection(string text)
+ {
+ // Validate
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ Func<bool> action = () =>
+ {
+ return ReplaceHelper(_textView.Selection.VirtualSelectedSpans, text);
+ };
+
+ return ExecuteAction(Strings.ReplaceSelectionWith + text, action);
+ }
+
+ /// <summary>
+ /// Replace text from the given span with the new text.
+ /// </summary>
+ /// <param name="span">
+ /// The span of text to replace.
+ /// </param>
+ /// <param name="text">
+ /// The new text that will replace the old selection.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/> is greater than the length in the TextBuffer.</exception>
+ public bool ReplaceText(Span span, string text)
+ {
+ // Validate
+ if (span.End > _textView.TextSnapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+
+ Func<bool> action = () =>
+ {
+ return ReplaceHelper(span, text);
+ };
+
+ return ExecuteAction(Strings.ReplaceText, action, SelectionUpdate.Reset, true);
+ }
+
+ /// <summary>
+ /// Replaces all matching occurrences of the given string.
+ /// </summary>
+ /// <param name="searchText">
+ /// Text to match.
+ /// </param>
+ /// <param name="replaceText">
+ /// Text used in replace.
+ /// </param>
+ /// <param name="matchCase">
+ /// True if search should match case, false otherwise.
+ /// </param>
+ /// <param name="matchWholeWord">
+ /// True if search should match whole word, false otherwise.
+ /// </param>
+ /// <param name="useRegularExpressions">
+ /// True if search should use regular expression, false otherwise.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="searchText"/> is null.</exception>
+ /// <remarks>If one of the matches found is read only, none of the matches will be replaced.</remarks>
+ public int ReplaceAllMatches(string searchText, string replaceText, bool matchCase, bool matchWholeWord, bool useRegularExpressions)
+ {
+ if (searchText == null)
+ {
+ throw new ArgumentNullException("searchText");
+ }
+
+ FindData findData = new FindData(searchText, _textView.TextSnapshot);
+ findData.TextStructureNavigator = _textStructureNavigator;
+ if (matchCase)
+ {
+ findData.FindOptions = findData.FindOptions | FindOptions.MatchCase;
+ }
+ if (matchWholeWord)
+ {
+ findData.FindOptions = findData.FindOptions | FindOptions.WholeWord;
+ }
+ if (useRegularExpressions)
+ {
+ findData.FindOptions = findData.FindOptions | FindOptions.UseRegularExpressions;
+ }
+
+ int numberOfReplacementsMade = 0;
+
+ // Now, use FindLogic to find all matching occurrences
+ Collection<SnapshotSpan> textSpanMatches = _factory.TextSearchService.FindAll(findData);
+
+ if (textSpanMatches != null && textSpanMatches.Count > 0)
+ {
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.ReplaceAll))
+ {
+ AddBeforeTextBufferChangePrimitive();
+
+ bool replaceFailed = false;
+ using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit())
+ {
+ // If we are not using regular expressions, simply replace all matches with replaceText
+ if (!useRegularExpressions)
+ {
+ // Perform each replace, and create an undo unit to track its changes
+ foreach (SnapshotSpan snapSpan in textSpanMatches)
+ {
+ if (!textEdit.Replace(snapSpan, replaceText))
+ {
+ replaceFailed = true;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Since we are using regular expressions, each replace text may be dependent on the matching text.
+ // Therefore, bring up a regular expression engine, and for each match, we will take that text and use Regex.Replace() on the string.
+ Regex regex = new Regex(searchText, (!matchCase ? RegexOptions.IgnoreCase : 0));
+
+ foreach (SnapshotSpan textSpan in textSpanMatches)
+ {
+ string newText = regex.Replace(textSpan.GetText(), replaceText);
+ if (!textEdit.Replace(textSpan, newText))
+ {
+ replaceFailed = true;
+ break;
+ }
+ }
+ }
+
+ AddAfterTextBufferChangePrimitive();
+
+ if (!replaceFailed)
+ {
+ textEdit.Apply();
+
+ if (!textEdit.Canceled)
+ {
+ numberOfReplacementsMade = textSpanMatches.Count;
+ undoTransaction.Complete();
+ }
+ }
+ }
+ }
+ }
+
+ return numberOfReplacementsMade;
+ }
+
+ /// <summary>
+ /// Copies the selected text to clip board.
+ /// </summary>
+ /// <exception cref="InsufficientMemoryException"> is thrown if there is not sufficient memory to complete the operation.</exception>
+ public bool CopySelection()
+ {
+ if (!_textView.Selection.IsEmpty)
+ {
+ return PrepareClipboardSelectionCopy().Invoke();
+ }
+ else if (!IsPointOnBlankViewLine(_editorPrimitives.Caret) ||
+ (_editorOptions.GetOptionValue<bool>(DefaultTextViewOptions.CutOrCopyBlankLineIfNoSelectionId)))
+ {
+ return PrepareClipboardFullLineCopy(GetFullLines()).Invoke();
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ public bool CutFullLine()
+ {
+ DisplayTextRange fullSelectedLines = GetFullLines();
+
+ if (!fullSelectedLines.TextBuffer.AdvancedTextBuffer.IsReadOnly(fullSelectedLines.AdvancedTextRange))
+ {
+ Func<bool> putCopiedLineToClipboard = PrepareClipboardFullLineCopy(fullSelectedLines);
+
+ return ExecuteAction(Strings.CutLine, () => fullSelectedLines.Delete() && putCopiedLineToClipboard.Invoke());
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Cuts the selected text.
+ /// </summary>
+ /// <exception cref="InsufficientMemoryException"> is thrown if there is not sufficient memory to complete the operation.</exception>
+ public bool CutSelection()
+ {
+ // no-op if can't do any cut operation
+ if (_textView.Selection.IsEmpty && !CanCut)
+ return true;
+
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.CutSelection))
+ {
+ this.AddBeforeTextBufferChangePrimitive();
+
+ bool performedCut = false;
+
+ if (!_textView.Selection.IsEmpty)
+ {
+ Func<bool> putCopiedSelectionToClipboard = PrepareClipboardSelectionCopy();
+
+ performedCut = this.Delete() && putCopiedSelectionToClipboard.Invoke();
+ }
+ else
+ {
+ DisplayTextRange lineRange = GetFullLines();
+ double caretX = _textView.Caret.Left;
+ Func<bool> putCopiedLineToClipboard = PrepareClipboardFullLineCopy(lineRange);
+
+ if (lineRange.Delete() && putCopiedLineToClipboard.Invoke())
+ {
+ performedCut = true;
+
+ // Move the caret back to the starting x coordinate on it's current line
+ _textView.Caret.MoveTo(_textView.Caret.ContainingTextViewLine, caretX);
+ }
+ }
+
+ if (performedCut)
+ {
+ _textView.Caret.EnsureVisible();
+ this.AddAfterTextBufferChangePrimitive();
+ undoTransaction.Complete();
+ }
+ else
+ {
+ undoTransaction.Cancel();
+ }
+
+ return performedCut;
+ }
+ }
+
+ /// <summary>
+ /// Pastes text from the clipboard to the text buffer.
+ /// </summary>
+ public bool Paste()
+ {
+ string text = null;
+ bool dataHasLineCutCopyTag = false;
+ bool dataHasBoxCutCopyTag = false;
+
+ // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block
+ try
+ {
+ IDataObject dataObj = Clipboard.GetDataObject();
+
+ if (dataObj == null || !dataObj.GetDataPresent(typeof(string)))
+ {
+ return true;
+ }
+
+ text = (string)dataObj.GetData(DataFormats.UnicodeText);
+ if (text == null)
+ {
+ text = (string)dataObj.GetData(DataFormats.Text);
+ }
+
+ dataHasLineCutCopyTag = dataObj.GetDataPresent(_clipboardLineBasedCutCopyTag);
+ dataHasBoxCutCopyTag = dataObj.GetDataPresent(_boxSelectionCutCopyTag);
+ }
+ catch (System.Runtime.InteropServices.ExternalException)
+ {
+ // TODO: Log error
+ return false;
+ }
+ catch (OutOfMemoryException)
+ {
+ // silently fail on out of memory exceptions.
+ // the clipboard also throws out of memory exceptions when the data in the clipboard is corrupt
+ // see bug 780687
+ return false;
+ }
+
+ if (text != null)
+ {
+ if (dataHasLineCutCopyTag && _textView.Selection.IsEmpty)
+ {
+ //this only applies if the data was copied from the editor itself in the special cut/copy
+ //mode when the caret is placed on a line and it's copied/cut without any selection
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.Paste))
+ {
+ this.AddBeforeTextBufferChangePrimitive();
+
+ SnapshotPoint insertionPoint = _textView.Caret.Position.BufferPosition.GetContainingLine().Start;
+
+ using (ITextEdit edit = _textView.TextBuffer.CreateEdit())
+ {
+ if (!edit.Insert(insertionPoint.Position, text))
+ return false;
+
+ edit.Apply();
+ }
+
+ this.AddAfterTextBufferChangePrimitive();
+
+ undoTransaction.Complete();
+
+ _textView.Caret.EnsureVisible();
+
+ return true;
+ }
+ }
+ else if (dataHasBoxCutCopyTag)
+ {
+ // If the caret is on a blank line, treat this almost like a normal stream
+ // insertion, but with extra whitespace on the beginning of each line to
+ // correctly maintain column position.
+ if (_textView.Selection.IsEmpty && IsPointOnBlankViewLine(_editorPrimitives.Caret))
+ {
+ // We want each line to be at the column position of the caret, which is
+ // the editor primitives caret column + virtual spaces.
+ string whitespace = GetWhitespaceForDisplayColumn(_editorPrimitives.Caret.Column + _textView.Caret.Position.VirtualBufferPosition.VirtualSpaces);
+
+ // Collect the lines from the clipboard
+ List<string> lines = new List<string>();
+ using (StringReader reader = new StringReader(text))
+ {
+ for (string lineText = reader.ReadLine(); lineText != null; lineText = reader.ReadLine())
+ {
+ lines.Add(lineText);
+ }
+ }
+
+ string streamText = string.Join(_editorOptions.GetNewLineCharacter() + whitespace, lines);
+ return this.InsertText(streamText.ToString(), true, Strings.Paste, isOverwriteModeEnabled: false);
+ }
+ else
+ {
+ VirtualSnapshotPoint unusedStart, unusedEnd;
+
+ return this.InsertTextAsBox(text, out unusedStart, out unusedEnd, Strings.Paste);
+ }
+ }
+ else
+ {
+ return this.InsertText(text, true, Strings.Paste, isOverwriteModeEnabled: false);
+ }
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets whether or not a paste operation can happen.
+ /// </summary>
+ public bool CanPaste
+ {
+ get
+ {
+ // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block
+ try
+ {
+ return Clipboard.ContainsText() && !_textView.TextSnapshot.TextBuffer.IsReadOnly(_editorPrimitives.Caret.CurrentPosition);
+ }
+ catch (System.Runtime.InteropServices.ExternalException)
+ {
+ // TODO: Log error
+ return false;
+ }
+ }
+ }
+
+ public bool CanDelete
+ {
+ get
+ {
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ if (_editorPrimitives.Caret.CurrentPosition < _textView.TextSnapshot.Length)
+ {
+ return !_textView.TextSnapshot.TextBuffer.IsReadOnly(_editorPrimitives.Caret.CurrentPosition);
+ }
+
+ return false;
+ }
+ else
+ {
+ return !_editorPrimitives.Selection.AdvancedTextRange.Snapshot.TextBuffer.IsReadOnly(_editorPrimitives.Selection.AdvancedTextRange);
+ }
+ }
+ }
+
+ public bool CanCut
+ {
+ get
+ {
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ if (!IsPointOnBlankViewLine(_editorPrimitives.Caret) ||
+ _editorOptions.GetOptionValue<bool>(DefaultTextViewOptions.CutOrCopyBlankLineIfNoSelectionId))
+ {
+ ITextViewLine caretLine = _editorPrimitives.Caret.AdvancedCaret.ContainingTextViewLine;
+ return !caretLine.Snapshot.TextBuffer.IsReadOnly(caretLine.ExtentIncludingLineBreak);
+ }
+ }
+ else
+ {
+ return !_editorPrimitives.Selection.AdvancedTextRange.Snapshot.TextBuffer.IsReadOnly(_editorPrimitives.Selection.AdvancedTextRange);
+ }
+
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Sets the caret at the start of the specified line.
+ /// </summary>
+ /// <param name="lineNumber">
+ /// The line number to set the caret at.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/>is less than 0 or greater than the line number of the last line in the TextBuffer.</exception>
+ public void GotoLine(int lineNumber)
+ {
+ // Validate
+ if (lineNumber < 0 || lineNumber > _textView.TextSnapshot.LineCount - 1)
+ throw new ArgumentOutOfRangeException("lineNumber");
+
+ ITextSnapshotLine line = _textView.TextSnapshot.GetLineFromLineNumber(lineNumber);
+
+ _textView.Caret.MoveTo(line.Start);
+ _textView.Selection.Clear();
+ _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0));
+ }
+
+ /// <summary>
+ /// The text view on which these operations work.
+ /// </summary>
+ public ITextView TextView
+ {
+ get
+ {
+ return _textView;
+ }
+ }
+
+ /// <summary>
+ /// Scrolls the view either up by one line,
+ /// reposition caret only if it is scrolled off the page.
+ /// It will reposition the cursor to the newly scrolled line
+ /// at the bottom of the view.
+ /// </summary>
+ public void ScrollUpAndMoveCaretIfNecessary()
+ {
+ this.ScrollByLineAndMoveCaretIfNecessary(ScrollDirection.Up);
+ }
+
+ public void SwapCaretAndAnchor()
+ {
+ _textView.Selection.Select(_textView.Selection.ActivePoint,
+ _textView.Selection.AnchorPoint);
+ _textView.Caret.MoveTo(_textView.Selection.ActivePoint);
+ _textView.Caret.EnsureVisible();
+ }
+
+ /// <summary>
+ /// Scrolls the view either up by one line,
+ /// reposition caret only if it is scrolled off the page.
+ /// It will reposition the cursor to the newly scrolled line
+ /// at the top of the view.
+ /// </summary>
+ public void ScrollDownAndMoveCaretIfNecessary()
+ {
+ this.ScrollByLineAndMoveCaretIfNecessary(ScrollDirection.Down);
+ }
+
+ /// <summary>
+ /// Transposes character at the cursor with next character.
+ /// </summary>
+ public bool TransposeCharacter()
+ {
+ return ExecuteAction(Strings.TransposeCharacter, () => _editorPrimitives.Caret.TransposeCharacter());
+ }
+
+ /// <summary>
+ /// Transposes line containing the cursor with the next line.
+ /// </summary>
+ public bool TransposeLine()
+ {
+ // Only transpose lines if there are more than 2 lines
+ if (_textView.TextSnapshot.LineCount < 2)
+ {
+ return true;
+ }
+
+ return ExecuteAction(Strings.TransposeLine, () => _editorPrimitives.Caret.TransposeLine());
+ }
+
+ public bool TransposeWord()
+ {
+ TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord();
+ if (currentWord.IsEmpty)
+ {
+ currentWord = currentWord.GetStartPoint().GetPreviousWord();
+ }
+
+ if (currentWord.GetEndPoint().CurrentPosition == _editorPrimitives.Buffer.GetEndPoint().CurrentPosition)
+ {
+ return true;
+ }
+
+ Func<bool> action = () =>
+ {
+ TextRange nextWord = currentWord.GetEndPoint().GetNextWord();
+
+ Func<TextRange, bool> keepSearching = (tr) =>
+ {
+ if (tr.GetEndPoint().CurrentPosition == _editorPrimitives.Buffer.GetEndPoint().CurrentPosition)
+ {
+ return false;
+ }
+ if (!tr.IsEmpty && char.IsLetterOrDigit(tr.GetText()[0]))
+ {
+ return false;
+ }
+ return true;
+ };
+
+ while (keepSearching(nextWord))
+ {
+ nextWord = nextWord.GetEndPoint().GetNextWord();
+ }
+
+ int newCaretPosition = nextWord.GetEndPoint().CurrentPosition;
+
+ using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit())
+ {
+ if (!textEdit.Replace(currentWord.AdvancedTextRange.Span, nextWord.GetText()))
+ return false;
+ if (!textEdit.Replace(nextWord.AdvancedTextRange.Span, currentWord.GetText()))
+ return false;
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+ }
+
+ _editorPrimitives.Caret.MoveTo(newCaretPosition);
+
+ return true;
+ };
+
+ return ExecuteAction(Strings.TransposeWord, action);
+ }
+
+ /// <summary>
+ /// Converts letters to the specified case in select text or next character to the cursor if select is empty.
+ /// </summary>
+ /// <param name="letterCase">
+ /// The letter case to convert to.
+ /// </param>
+ private bool ChangeCase(LetterCase letterCase)
+ {
+ NormalizedSnapshotSpanCollection spans;
+ SelectionUpdate selectionUpdate;
+ if (_textView.Selection.IsEmpty)
+ {
+ // Do nothing if caret is at the end of buffer and there is no selection
+ if (_textView.Caret.Position.BufferPosition == _textView.TextSnapshot.Length)
+ return true;
+
+ // If the caret is at the physical end of a line, just move to the next line.
+ VirtualSnapshotPoint caret = _textView.Caret.Position.VirtualBufferPosition;
+ ITextSnapshotLine line = _textView.TextSnapshot.GetLineFromPosition(caret.Position);
+
+ if (line.End == caret.Position)
+ {
+ _textView.Caret.MoveTo(line.EndIncludingLineBreak);
+ return true;
+ }
+
+ spans = new NormalizedSnapshotSpanCollection(_textView.GetTextElementSpan(caret.Position));
+ selectionUpdate = SelectionUpdate.Ignore;
+ }
+ else
+ {
+ spans = _textView.Selection.SelectedSpans;
+ selectionUpdate = SelectionUpdate.Preserve;
+ }
+
+ Func<bool> action = () =>
+ {
+ return EditHelper((edit) =>
+ {
+ foreach (var span in spans)
+ {
+ string textToConvert = span.GetText();
+ string convertedCaseText = (letterCase == LetterCase.Uppercase)
+ ? textToConvert.ToUpper(System.Globalization.CultureInfo.CurrentCulture)
+ : textToConvert.ToLower(System.Globalization.CultureInfo.CurrentCulture);
+
+ if (!edit.Replace(span, convertedCaseText))
+ return false;
+ }
+
+ return true;
+ });
+
+ };
+ return this.ExecuteAction((letterCase == LetterCase.Uppercase ? Strings.MakeUppercase : Strings.MakeLowercase), action, selectionUpdate, true);
+ }
+
+ /// <summary>
+ /// Converts lowercase letters to uppercase in select text or next character to the cursor if select is empty.
+ /// </summary>
+ public bool MakeUppercase()
+ {
+ return ChangeCase(LetterCase.Uppercase);
+ }
+
+ /// <summary>
+ /// Converts uppercase letters to lowercase in select text or next character to the cursor if select is empty.
+ /// </summary>
+ public bool MakeLowercase()
+ {
+ return ChangeCase(LetterCase.Lowercase);
+ }
+
+ public bool Capitalize()
+ {
+ return ExecuteAction(Strings.Capitalize, () => _editorPrimitives.Selection.Capitalize());
+ }
+
+ public bool ToggleCase()
+ {
+ return ExecuteAction(Strings.ToggleCase, () => _editorPrimitives.Selection.ToggleCase());
+ }
+
+ public bool InsertFile(string filePath)
+ {
+ Func<bool> action =
+ () =>
+ {
+ IContentType contentTypeInert = _factory.ContentTypeRegistryService.GetContentType("inert");
+ bool unused;
+ ITextDocument document = _factory.TextDocumentFactoryService.CreateAndLoadTextDocument(filePath, contentTypeInert, attemptUtf8Detection: true, characterSubstitutionsOccurred: out unused);
+ ITrackingPoint oldCaretLocation = _textView.TextSnapshot.CreateTrackingPoint(_editorPrimitives.Caret.CurrentPosition, PointTrackingMode.Negative);
+ if (!_editorPrimitives.Caret.InsertText(document.TextBuffer.CurrentSnapshot.GetText()))
+ return false;
+ _editorPrimitives.Caret.MoveTo(oldCaretLocation.GetPosition(_textView.TextSnapshot));
+ return true;
+ };
+
+ return ExecuteAction(Strings.InsertFile, action);
+ }
+
+ public void ScrollPageUp()
+ {
+ _editorPrimitives.View.ScrollPageUp();
+ }
+
+ public void ScrollPageDown()
+ {
+ _editorPrimitives.View.ScrollPageDown();
+ }
+
+ public void ScrollColumnLeft()
+ {
+ IWpfTextView wpfTextView = _textView as IWpfTextView;
+ if (wpfTextView != null)
+ {
+ // A column is defined as the width of a space in the default font
+ _textView.ViewScroller.ScrollViewportHorizontallyByPixels(wpfTextView.FormattedLineSource.ColumnWidth * -1.0);
+ }
+ }
+
+ public void ScrollColumnRight()
+ {
+ IWpfTextView wpfTextView = _textView as IWpfTextView;
+ if (wpfTextView != null)
+ {
+ // A column is defined as the width of a space in the default font
+ _textView.ViewScroller.ScrollViewportHorizontallyByPixels(wpfTextView.FormattedLineSource.ColumnWidth);
+ }
+ }
+
+ public void ScrollLineBottom()
+ {
+ _editorPrimitives.View.MoveLineToBottom(_editorPrimitives.Caret.LineNumber);
+ }
+
+ public void ScrollLineTop()
+ {
+ _editorPrimitives.View.MoveLineToTop(_editorPrimitives.Caret.LineNumber);
+ }
+
+ public void ScrollLineCenter()
+ {
+ _editorPrimitives.View.Show(_editorPrimitives.Caret, HowToShow.Centered);
+ }
+
+ public void AddAfterTextBufferChangePrimitive()
+ {
+ // Create an AfterTextBufferChangeUndoPrimitive undo primitive using the current caret position and selection.
+ AfterTextBufferChangeUndoPrimitive afterTextBufferChangeUndoPrimitive =
+ AfterTextBufferChangeUndoPrimitive.Create(_textView, _undoHistory);
+ _undoHistory.CurrentTransaction.AddUndo(afterTextBufferChangeUndoPrimitive);
+ }
+
+ public void AddBeforeTextBufferChangePrimitive()
+ {
+ // Create a BeforeTextBufferChangeUndoPrimitive undo primitive using the current caret position and selection.
+ BeforeTextBufferChangeUndoPrimitive beforeTextBufferChangeUndoPrimitive =
+ BeforeTextBufferChangeUndoPrimitive.Create(_textView, _undoHistory);
+ _undoHistory.CurrentTransaction.AddUndo(beforeTextBufferChangeUndoPrimitive);
+ }
+
+ public void ZoomIn()
+ {
+ IWpfTextView wpfTextView = _textView as IWpfTextView;
+ if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable))
+ {
+ double zoomLevel = wpfTextView.ZoomLevel * ZoomConstants.ScalingFactor;
+ if (zoomLevel < ZoomConstants.MaxZoom || Math.Abs(zoomLevel - ZoomConstants.MaxZoom) < 0.00001)
+ {
+ wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel);
+ }
+ }
+ }
+
+ public void ZoomOut()
+ {
+ IWpfTextView wpfTextView = _textView as IWpfTextView;
+ if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable))
+ {
+ double zoomLevel = wpfTextView.ZoomLevel / ZoomConstants.ScalingFactor;
+ if (zoomLevel > ZoomConstants.MinZoom || Math.Abs(zoomLevel - ZoomConstants.MinZoom) < 0.00001)
+ {
+ wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel);
+ }
+ }
+ }
+
+ public void ZoomTo(double zoomLevel)
+ {
+ IWpfTextView wpfTextView = _textView as IWpfTextView;
+ if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable))
+ {
+ wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel);
+ }
+ }
+
+ #endregion // IEditorOperations Members
+
+ #region Virtual Space to Whitespace helpers
+
+ public string GetWhitespaceForVirtualSpace(VirtualSnapshotPoint point)
+ {
+ if (!point.IsInVirtualSpace)
+ return string.Empty;
+
+ return GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces);
+ }
+
+ internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces)
+ {
+ return GetWhiteSpaceForPositionAndVirtualSpace(position, virtualSpaces, useBufferPrimitives: false);
+ }
+
+ internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces, bool useBufferPrimitives)
+ {
+ return GetWhiteSpaceForPositionAndVirtualSpace(position, virtualSpaces, useBufferPrimitives, convertTabsToSpaces: _textView.Options.IsConvertTabsToSpacesEnabled());
+ }
+
+ /// <summary>
+ /// Given a buffer point and a count of virtual spaces, determine the correct whitespace, as a sequence of tabs/spaces, to get
+ /// from the buffer point to the virtual point. Note that the buffer position doesn't have to be at the end of the line, so this
+ /// method can be used to calculate whitespace offset in arbitrary contexts.
+ /// </summary>
+ /// <param name="position">The buffer position to start from.</param>
+ /// <param name="virtualSpaces">The count of virtual spaces to generate whitespace for.</param>
+ /// <param name="useBufferPrimitives">Whether to use buffer or view primitives. Buffer primitives must be used if the given
+ /// <paramref name="position"/> could be hidden in the view.</param>
+ /// <param name="convertTabsToSpaces">Whether tabs or spaces should be use.</param>
+ /// <returns>A non-null string of whitespace.</returns>
+ internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces, bool useBufferPrimitives, bool convertTabsToSpaces)
+ {
+ string textToInsert;
+ if (!convertTabsToSpaces)
+ {
+ int tabSize = _textView.Options.GetTabSize();
+
+ int columnOfEndOfLine;
+ if (useBufferPrimitives)
+ columnOfEndOfLine = _editorPrimitives.Buffer.GetTextPoint(position).Column;
+ else
+ columnOfEndOfLine = _editorPrimitives.View.GetTextPoint(position).DisplayColumn;
+
+ int caretColumn = columnOfEndOfLine + virtualSpaces;
+
+ int spacesAfterPreviousTabStop = caretColumn % tabSize;
+ int columnOfPreviousTabStop = caretColumn - spacesAfterPreviousTabStop;
+
+ int requiredTabs = ((columnOfPreviousTabStop - columnOfEndOfLine) + tabSize - 1) / tabSize;
+
+ if (requiredTabs > 0)
+ textToInsert = new string('\t', requiredTabs) + new string(' ', spacesAfterPreviousTabStop);
+ else
+ textToInsert = new string(' ', virtualSpaces);
+ }
+ else
+ {
+ textToInsert = new string(' ', virtualSpaces);
+ }
+
+ return textToInsert;
+ }
+
+ internal string GetWhitespaceForDisplayColumn(int caretColumn)
+ {
+ string textToInsert;
+ if (!_textView.Options.IsConvertTabsToSpacesEnabled())
+ {
+ int tabSize = _textView.Options.GetTabSize();
+
+ int spacesToInsert = caretColumn % tabSize;
+ textToInsert = new string(' ', spacesToInsert);
+
+ int columnOfPreviousTabStop = caretColumn - spacesToInsert;
+
+ int requiredTabs = ((columnOfPreviousTabStop) + tabSize - 1) / tabSize;
+
+ if (requiredTabs > 0)
+ textToInsert = new string('\t', requiredTabs) + textToInsert;
+ }
+ else
+ {
+ textToInsert = new string(' ', caretColumn);
+ }
+
+ return textToInsert;
+ }
+
+ #endregion
+
+ #region Text insertion helpers
+
+ private bool InsertText(string text, bool final)
+ {
+ return this.InsertText(text, final, Strings.InsertText, _textView.Caret.OverwriteMode);
+ }
+
+ private bool InsertText(string text, bool final, string undoText, bool isOverwriteModeEnabled)
+ {
+ // Validate
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ if ((text.Length == 0) && !final)
+ {
+ throw new ArgumentException("Provisional TextInput cannot be zero-length");
+ }
+
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(undoText))
+ {
+ // Determine undo merge direction(s). By default we allow merging, but in two cases we don't want to merge
+ // with previous transactions:
+ // 1. If the document is clean (else you won't be able to undo back to a clean state (Dev10 672382)).
+ // 2. If we are replacing selected text (replacing selected text should be an atomic undo operation).
+ var mergeDirections = TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward;
+ if ((!_textView.Selection.IsEmpty && !IsEmptyBoxSelection()) // replacing selected text.
+ || (_textDocument != null && !_textDocument.IsDirty)) // document is clean.
+ {
+ mergeDirections = TextTransactionMergeDirections.Forward;
+ }
+ undoTransaction.MergePolicy = new TextTransactionMergePolicy(mergeDirections);
+
+ this.AddBeforeTextBufferChangePrimitive();
+
+ IEnumerable<VirtualSnapshotSpan> replaceSpans;
+
+ TextEditAction action = TextEditAction.Type;
+
+ // If this is a non-empty stream selection and *not* an empty box
+ if (!_textView.Selection.IsEmpty && _immProvisionalComposition == null)
+ {
+ // If the caret is in overwrite mode and this is an empty box, treat
+ // it like an overwrite on each line.
+ if (_textView.Options.IsOverwriteModeEnabled() && IsEmptyBoxSelection())
+ {
+ List<VirtualSnapshotSpan> spans = new List<VirtualSnapshotSpan>();
+
+ foreach (var span in _textView.Selection.VirtualSelectedSpans)
+ {
+ SnapshotPoint startPoint = span.Start.Position;
+
+ if (span.Start.IsInVirtualSpace ||
+ startPoint.GetContainingLine().End == startPoint)
+ {
+ spans.Add(span);
+ }
+ else
+ {
+ spans.Add(new VirtualSnapshotSpan(
+ new SnapshotSpan(startPoint, _textView.GetTextElementSpan(startPoint).End)));
+ }
+ }
+
+ replaceSpans = spans;
+ }
+ else
+ {
+ replaceSpans = _textView.Selection.VirtualSelectedSpans;
+ }
+
+ // The provisional composition span should be null here (the IME should
+ // finalize before doing anything that could cause a selection) but we treat
+ // this as a soft error since something time sliced on the UI thread could
+ // affect the selection. So we simply ignore the old provisional span.
+ }
+ else if (_immProvisionalComposition != null)
+ {
+ SnapshotSpan provisionalSpan = _immProvisionalComposition.GetSpan(_textView.TextSnapshot);
+
+ if (IsEmptyBoxSelection() && final)
+ {
+ // For an empty box, replace the equivalent to the provisional span
+ // on each line
+ IList<VirtualSnapshotSpan> spans = new List<VirtualSnapshotSpan>();
+ foreach (SnapshotPoint start in _textView.Selection.VirtualSelectedSpans.Select(s => s.Start.Position))
+ {
+ if (start.Position - provisionalSpan.Length >= 0)
+ {
+ spans.Add(new VirtualSnapshotSpan(
+ new SnapshotSpan(start - provisionalSpan.Length, start)));
+ }
+ else
+ {
+ Debug.Fail("Asked to replace a provisional span that would go past the beginning of the buffer; ignoring.");
+ }
+ }
+
+ replaceSpans = spans;
+ }
+ else
+ {
+ replaceSpans = new VirtualSnapshotSpan[] {
+ new VirtualSnapshotSpan(provisionalSpan) };
+ }
+
+ // The length of the replacement should always be > 0 but there are scenarios
+ // (such as a replace by some asynchronous command time-sliced to the UI thread)
+ // that could zero out the span.
+
+ action = TextEditAction.ProvisionalOverwrite;
+ }
+ else
+ {
+ VirtualSnapshotPoint insertionPoint = _textView.Caret.Position.VirtualBufferPosition;
+ if (isOverwriteModeEnabled && !insertionPoint.IsInVirtualSpace)
+ {
+ SnapshotPoint point = insertionPoint.Position;
+ replaceSpans = new VirtualSnapshotSpan[] { new VirtualSnapshotSpan(
+ new SnapshotSpan(point, _textView.GetTextElementSpan(point).End)) };
+ }
+ else
+ {
+ replaceSpans = new VirtualSnapshotSpan[] {
+ new VirtualSnapshotSpan(insertionPoint, insertionPoint) };
+ }
+ }
+
+ ITextVersion currentVersion = _textView.TextSnapshot.Version;
+ ITrackingSpan newProvisionalSpan = null;
+
+ bool editSuccessful = true;
+
+ int? firstLineInsertedVirtualSpaces = null;
+ int lastLineInsertedVirtualSpaces = 0;
+
+ ITrackingPoint endPoint = null;
+
+ using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit(EditOptions.None, null, action))
+ {
+ bool firstSpan = true;
+ foreach (var span in replaceSpans)
+ {
+ string lineText = text;
+
+ // Remember the endPoint for a possible provisional span.
+ // Use negative tracking, so it doesn't move with the newly inserted text.
+ endPoint = _textView.TextSnapshot.CreateTrackingPoint(span.Start.Position, PointTrackingMode.Negative);
+
+ if (span.Start.IsInVirtualSpace)
+ {
+ string whitespace = GetWhitespaceForVirtualSpace(span.Start);
+ if (firstSpan)
+ {
+ //Keep track of the number of whitespace characters -- which might include tabs -- so that the caller can figure out where the inserted text
+ //actually lands.
+ _textView.TextBuffer.Properties["WhitespaceInserted"] = whitespace.Length;
+ }
+ lineText = whitespace + text;
+ }
+
+ if (!firstLineInsertedVirtualSpaces.HasValue)
+ firstLineInsertedVirtualSpaces = lineText.Length - text.Length;
+
+ lastLineInsertedVirtualSpaces = lineText.Length - text.Length;
+
+ if (!textEdit.Replace(span.SnapshotSpan, lineText) || textEdit.Canceled)
+ {
+ editSuccessful = false;
+ break;
+ }
+
+ firstSpan = false;
+ }
+
+ if (editSuccessful)
+ {
+ textEdit.Apply();
+ editSuccessful = !textEdit.Canceled;
+ }
+ }
+
+ if (editSuccessful)
+ {
+ // Get rid of virtual space if there is any,
+ // since we've just made it non-virtual
+ _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition);
+ _textView.Selection.Select(
+ new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position),
+ new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position));
+
+ // If the selection ends up being non-empty (meaning not an empty
+ // single selection *or* an empty box), then clear it.
+ if (_textView.Selection.VirtualSelectedSpans.Any(s => !s.IsEmpty))
+ _textView.Selection.Clear();
+
+ _textView.Caret.EnsureVisible();
+
+ this.AddAfterTextBufferChangePrimitive();
+
+ undoTransaction.Complete();
+
+ if (final)
+ {
+ newProvisionalSpan = null;
+ }
+ else if (_textView.Selection.IsReversed)
+ {
+ // The active point is at the start.
+ int virtualSpaces = firstLineInsertedVirtualSpaces.HasValue ? firstLineInsertedVirtualSpaces.Value : 0;
+
+ newProvisionalSpan = currentVersion.Next.CreateTrackingSpan(
+ new Span(replaceSpans.First().Start.Position + virtualSpaces, text.Length),
+ SpanTrackingMode.EdgeExclusive);
+ }
+ else
+ {
+ // The active point is at the end.
+ // Since text may have been inserted before this point, we need to accommodate the
+ // amount of inserted text, using the endPoint we constructed earlier.
+
+ int lastSpanStart = endPoint.GetPoint(_textView.TextSnapshot).Position;
+
+ newProvisionalSpan = currentVersion.Next.CreateTrackingSpan(
+ new Span(lastSpanStart + lastLineInsertedVirtualSpaces, text.Length),
+ SpanTrackingMode.EdgeExclusive);
+ }
+ }
+
+ // This test is for null to non-null or vice versa (two different ITrackingSpans -- even if they cover the same span -- are not
+ // considered equal).
+ if (_immProvisionalComposition != newProvisionalSpan)
+ {
+ _immProvisionalComposition = newProvisionalSpan;
+ _textView.ProvisionalTextHighlight = _immProvisionalComposition;
+ }
+
+ return editSuccessful;
+ }
+ }
+
+ public bool InsertTextAsBox(string text, out VirtualSnapshotPoint boxStart, out VirtualSnapshotPoint boxEnd)
+ {
+ return InsertTextAsBox(text, out boxStart, out boxEnd, Strings.InsertText);
+ }
+
+ public bool InsertTextAsBox(string text, out VirtualSnapshotPoint boxStart, out VirtualSnapshotPoint boxEnd, string undoText)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+
+ boxStart = boxEnd = _textView.Caret.Position.VirtualBufferPosition;
+
+ // You can't use out parameters in a lambda or delegate, so just make a copy of
+ // boxStart/boxEnd
+ VirtualSnapshotPoint newStart, newEnd;
+ newStart = newEnd = boxStart;
+
+ if (text.Length == 0)
+ return this.InsertText(text);
+
+ Func<bool> action = () =>
+ {
+ // Separate edit transaction for the delete
+ if (!DeleteHelper(_textView.Selection.SelectedSpans))
+ return false;
+
+ // Put the selection in box mode for this operation
+ _textView.Selection.Mode = TextSelectionMode.Box;
+
+ _textView.Caret.MoveTo(_textView.Selection.Start);
+
+ // Remember the starting position
+ VirtualSnapshotPoint oldCaretPos = _textView.Caret.Position.VirtualBufferPosition;
+ double startX = _textView.Caret.Left;
+
+ // Estimate the number of spaces to the caret, to be used
+ // for any amount of text that goes past the end of the buffer
+ int spacesToCaret = _editorPrimitives.View.GetTextPoint(oldCaretPos.Position).DisplayColumn + oldCaretPos.VirtualSpaces;
+
+ ITrackingPoint endPoint = null;
+ int? firstLineInsertedVirtualSpaces = null;
+
+ using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit())
+ {
+ VirtualSnapshotPoint currentCaret = oldCaretPos;
+ ITextViewLine currentLine = _textView.GetTextViewLineContainingBufferPosition(currentCaret.Position);
+
+ bool pastEndOfBuffer = false;
+
+ // Read a line at a time from the given text, inserting each line of text
+ // into the buffer at a successive line below the line the caret is on.
+ // For any text left over at the end of the buffer, insert endlines as
+ // well (to create new lines at the end of the file), with virtual space
+ // for padding.
+ using (StringReader reader = new StringReader(text))
+ {
+ for (string lineText = reader.ReadLine(); lineText != null;)
+ {
+ if (lineText.Length > 0)
+ {
+ // Remember the endPoint, for determining the boxEnd argument
+ endPoint = _textView.TextSnapshot.CreateTrackingPoint(currentCaret.Position, PointTrackingMode.Positive);
+
+ int whitespaceLength = 0;
+
+ // Add on any virtual space needed
+ if (currentCaret.IsInVirtualSpace)
+ {
+ string whitespace = GetWhitespaceForVirtualSpace(currentCaret);
+ lineText = whitespace + lineText;
+ whitespaceLength = whitespace.Length;
+ }
+
+ // Update information about the first inserted line
+ if (!firstLineInsertedVirtualSpaces.HasValue)
+ {
+ firstLineInsertedVirtualSpaces = whitespaceLength;
+ oldCaretPos = currentCaret;
+ }
+
+ // Insert the text as part of this edit transaction
+ if (!textEdit.Insert(currentCaret.Position, lineText))
+ return false;
+ }
+
+ // We've already read past the end of the buffer, so we're done
+ if (pastEndOfBuffer)
+ break;
+
+ // If this is the last line in the file, collect the rest
+ // of the text to insert.
+ if (currentLine.LineBreakLength == 0)
+ {
+ string whitespace = GetWhitespaceForDisplayColumn(spacesToCaret);
+ string newline = _editorOptions.GetNewLineCharacter();
+
+ string extraLine = null;
+ lineText = string.Empty;
+ string blankLines = string.Empty;
+ while ((extraLine = reader.ReadLine()) != null)
+ {
+ // Either add the line (if there is any text), or
+ // just add the newline
+ if (extraLine.Length > 0)
+ {
+ lineText += blankLines + // Any blank lines
+ newline + // a line break, to get a new line
+ whitespace + // realized virtual space
+ extraLine; // the line text itself
+
+ blankLines = string.Empty;
+ }
+ else
+ {
+ blankLines += newline;
+ }
+ }
+
+ pastEndOfBuffer = true;
+
+ currentCaret = new VirtualSnapshotPoint(currentLine.EndIncludingLineBreak);
+ // Current line hasn't changed, so we don't need to set it again
+ }
+ else // Otherwise, try and read the next line
+ {
+ lineText = reader.ReadLine();
+
+ if (lineText != null)
+ {
+ currentLine = _textView.GetTextViewLineContainingBufferPosition(currentLine.EndIncludingLineBreak);
+ currentCaret = currentLine.GetInsertionBufferPositionFromXCoordinate(startX);
+ }
+ }
+ }
+ }
+
+ // If we didn't actually insert any text, then cancel the edit and return false
+ if (endPoint == null)
+ {
+ textEdit.Cancel();
+ return false;
+ }
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+ }
+
+ // Now, figure out the start and end positions
+ // and update the caret and selection.
+ _textView.Selection.Clear();
+
+ // Move the caret back to the starting point, which is now
+ // no longer in virtual space.
+ int virtualSpaces = firstLineInsertedVirtualSpaces.HasValue ? firstLineInsertedVirtualSpaces.Value : 0;
+
+ _textView.Caret.MoveTo(new SnapshotPoint(_textView.TextSnapshot,
+ oldCaretPos.Position.Position + virtualSpaces));
+
+ newStart = _textView.Caret.Position.VirtualBufferPosition;
+
+ // endPoint was a forward-tracking point at the beginning of the last insertion,
+ // so it should be positioned at the very end of the inserted text in the new
+ // snapshot.
+ newEnd = new VirtualSnapshotPoint(endPoint.GetPoint(_textView.TextSnapshot));
+
+ return true;
+ };
+
+ bool succeeded = ExecuteAction(undoText, action);
+
+ if (succeeded)
+ {
+ boxStart = newStart;
+ boxEnd = newEnd;
+ }
+
+ return succeeded;
+ }
+ #endregion
+
+ #region Clipboard and RTF helpers
+
+ private Func<bool> PrepareClipboardSelectionCopy()
+ {
+ NormalizedSnapshotSpanCollection selectedSpans = _textView.Selection.SelectedSpans;
+
+ string text = this.SelectedText;
+ string rtfText = null;
+ try
+ {
+ rtfText = GenerateRtf(selectedSpans);
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore cancellation when doing a copy. The user may not even want RTF text so preventing the normal text from being copied would be overkill.
+ }
+
+ bool isBox = _textView.Selection.Mode == TextSelectionMode.Box;
+
+ return () => CopyToClipboard(text, rtfText, lineCutCopyTag: false, boxCutCopyTag: isBox);
+ }
+
+ private Func<bool> PrepareClipboardFullLineCopy(DisplayTextRange textRange)
+ {
+ string text = textRange.GetText();
+
+ string rtfText = null;
+ try
+ {
+ rtfText = GenerateRtf(textRange.AdvancedTextRange);
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore cancellation when doing a copy. The user may not even want RTF text so preventing the normal text from being copied would be overkill.
+ }
+
+
+ return () => CopyToClipboard(text, rtfText, lineCutCopyTag: true, boxCutCopyTag: false);
+ }
+
+ /// <summary>
+ /// Copies data into the clipboard.
+ /// </summary>
+ /// <param name="textData">The textual content to put in the clipboard</param>
+ /// <param name="rtfData">The RTF formatted data to put in the clipboard</param>
+ /// <returns>true if operation succeeded.</returns>
+ private static bool CopyToClipboard(string textData, string rtfData, bool lineCutCopyTag, bool boxCutCopyTag)
+ {
+ // note: we should change clipboard data even if textData or rtfData are empty
+
+ // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block
+ try
+ {
+ DataObject dataObject = new DataObject();
+
+ //set plain text format
+ dataObject.SetText(textData);
+
+ //set any additional data
+ if (rtfData != null)
+ {
+ dataObject.SetData(DataFormats.Rtf, rtfData);
+ }
+
+ //tag the data in the clipboard if requested
+ if (lineCutCopyTag)
+ {
+ dataObject.SetData(_clipboardLineBasedCutCopyTag, true);
+ }
+
+ if (boxCutCopyTag)
+ {
+ dataObject.SetData(_boxSelectionCutCopyTag, true);
+ }
+
+ // When adding an item to the clipboard, use delay rendering. We expect the host to flush
+ // the data down during shutdown if it so desires. Putting the data on the clipboard with delay
+ // rendering helps with clipboard contention issues with remote desktop and multiple clipboard
+ // chain listeners.
+ // WPF, when doing no delay rendering, calls OleSetClipboard and OleFlushClipboard under the covers
+ // which causes the OS to send out two almost simultaneous clipboard open/close notification pairs
+ // which confuse applications that try to synchronize clipboard data between multiple machines such
+ // as MagicMouse or remote desktop.
+ Clipboard.SetDataObject(dataObject, false);
+
+ return true;
+ }
+ catch (System.Runtime.InteropServices.ExternalException)
+ {
+ // TODO: Log error
+ return false;
+ }
+ }
+
+ private string GenerateRtf(NormalizedSnapshotSpanCollection spans)
+ {
+ //Don't generate RTF for large spans (since it is expensive and probably not wanted).
+ int length = spans.Sum((span) => span.Length);
+ if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey))
+ {
+ if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey))
+ {
+ using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage))
+ {
+ return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken);
+ }
+ }
+ else
+ {
+ return _factory.RtfBuilderService.GenerateRtf(spans);
+ }
+ }
+ else
+ return null;
+ }
+
+ private string GenerateRtf(SnapshotSpan span)
+ {
+ return GenerateRtf(new NormalizedSnapshotSpanCollection(span));
+ }
+
+ #endregion
+
+ #region Horizontal whitespace helpers
+
+ private bool DeleteHorizontalWhitespace()
+ {
+ List<Span> singleSpansToDelete = new List<Span>();
+ List<Span> largeSpansToDelete = new List<Span>();
+ List<Span> singleTabsToReplace = new List<Span>();
+ ITextSnapshot snapshot = _editorPrimitives.View.AdvancedTextView.TextSnapshot;
+ int whitespaceLocation = -1;
+ ITrackingPoint lastDeleteLocation = snapshot.CreateTrackingPoint(_editorPrimitives.Caret.CurrentPosition, PointTrackingMode.Positive);
+
+ using (ITextEdit textEdit = snapshot.TextBuffer.CreateEdit())
+ {
+ int startPoint = GetStartPointOfSpanForDeleteHorizontalWhitespace(snapshot);
+ int endPoint = GetEndPointOfSpanForDeleteHorizontalWhitespace(snapshot);
+
+ for (int i = startPoint; i <= endPoint && i < snapshot.Length; i++)
+ {
+ if (IsSpaceCharacter(snapshot[i]))
+ {
+ if (whitespaceLocation == -1)
+ {
+ whitespaceLocation = i;
+ }
+ }
+ else
+ {
+ if (whitespaceLocation != -1)
+ {
+ if ((i - whitespaceLocation == 1) && !_editorPrimitives.Selection.IsEmpty)
+ {
+ // This is a span of just a single space or tab so track it as such
+ singleSpansToDelete.Add(Span.FromBounds(whitespaceLocation, i));
+
+ // If this is a tab character we need to replace it with a space
+ if (snapshot[i - 1] == '\t')
+ {
+ lastDeleteLocation = snapshot.CreateTrackingPoint(i, PointTrackingMode.Positive);
+ singleTabsToReplace.Add(Span.FromBounds(whitespaceLocation, i));
+ }
+ }
+ else
+ {
+ // This is a span of more than one space or tab so track it as such
+ lastDeleteLocation = snapshot.CreateTrackingPoint(i, PointTrackingMode.Positive);
+ largeSpansToDelete.Add(Span.FromBounds(whitespaceLocation, i));
+ }
+ }
+ whitespaceLocation = -1;
+ }
+ }
+
+ // If the last span is at end of the selection
+ if (whitespaceLocation != -1)
+ {
+ lastDeleteLocation = snapshot.CreateTrackingPoint(endPoint, PointTrackingMode.Positive);
+ if ((endPoint - whitespaceLocation == 1) && !_editorPrimitives.Selection.IsEmpty)
+ singleSpansToDelete.Add(Span.FromBounds(whitespaceLocation, endPoint));
+ else
+ largeSpansToDelete.Add(Span.FromBounds(whitespaceLocation, endPoint));
+ }
+
+ if (!DeleteHorizontalWhitespace(textEdit, largeSpansToDelete, singleSpansToDelete, singleTabsToReplace))
+ return false;
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ // Restore the caret to the x-coordinate it was at previously on the new line
+ _editorPrimitives.Caret.MoveTo(lastDeleteLocation.GetPosition(_editorPrimitives.View.AdvancedTextView.TextSnapshot));
+ }
+ }
+
+ return true;
+ }
+
+ private static bool DeleteHorizontalWhitespace(ITextEdit textEdit, ICollection<Span> largeSpansToDelete, ICollection<Span> singleSpansToDelete, ICollection<Span> singleTabsToReplace)
+ {
+ ITextSnapshot snapshot = textEdit.Snapshot;
+ Func<int, bool> isLeadingWhitespace = (p) => snapshot.GetLineFromPosition(p).Start == p;
+ Func<int, bool> isTrailingWhitespace = (p) => snapshot.GetLineFromPosition(p).End == p;
+
+ if (largeSpansToDelete.Count == 0)
+ {
+ // If there were no spans of more than one space or tab,
+ // then delete all spans of just a single space or tab
+ foreach (var span in singleSpansToDelete)
+ {
+ if (!textEdit.Delete(span))
+ return false;
+ }
+ }
+ else
+ {
+ // If there was at least one span of more than one space or
+ // tab then replace all of those spans with spaces
+ foreach (var span in largeSpansToDelete)
+ {
+ // if it is leading or trailing whitespace, just delete it
+ if (isLeadingWhitespace(span.Start) || isTrailingWhitespace(span.End))
+ {
+ if (!textEdit.Delete(span))
+ return false;
+ }
+ // if there's space adjacent to the beginning/end of the selection
+ else if (IsSpaceCharacter(snapshot[span.Start - 1]) || IsSpaceCharacter(snapshot[span.End]))
+ {
+ if (!textEdit.Delete(span))
+ return false;
+ }
+ else if (!textEdit.Replace(span, " "))
+ return false;
+ }
+
+ // Replace single tabs with a single space, but
+ // skip leading / trailing tabs since they will be deleted below by the singleSpansToDelete loop.
+ foreach (var span in singleTabsToReplace)
+ {
+ if (!isLeadingWhitespace(span.Start) && !isTrailingWhitespace(span.End))
+ {
+ if (!textEdit.Replace(span, " "))
+ return false;
+ }
+ }
+
+ // Delete leading or trailing whitespace in singleSpansToDelete
+ foreach (var span in singleSpansToDelete)
+ {
+ if (isLeadingWhitespace(span.Start) || isTrailingWhitespace(span.End))
+ {
+ if (!textEdit.Delete(span))
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private int GetEndPointOfSpanForDeleteHorizontalWhitespace(ITextSnapshot snapshot)
+ {
+ int endPoint = _editorPrimitives.Selection.GetEndPoint().CurrentPosition;
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ int endOfLine = _editorPrimitives.Caret.EndOfViewLine;
+ while (endPoint < endOfLine)
+ {
+ if (!IsSpaceCharacter(snapshot[endPoint]))
+ {
+ break;
+ }
+ endPoint++;
+ }
+ }
+ return endPoint;
+ }
+
+ private int GetStartPointOfSpanForDeleteHorizontalWhitespace(ITextSnapshot snapshot)
+ {
+ int startPoint = _editorPrimitives.Selection.GetStartPoint().CurrentPosition;
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ int startOfViewLine = _editorPrimitives.Caret.StartOfViewLine;
+ while (startPoint > startOfViewLine)
+ {
+ // If the caracter *before* the current start point is
+ // not whitespace, then quit.
+ if (!IsSpaceCharacter(snapshot[startPoint - 1]))
+ {
+ break;
+ }
+
+ startPoint--;
+ }
+ }
+ return startPoint;
+ }
+
+ #endregion
+
+ #region Indent/unindent helpers
+
+ // Perform the given indent action (indent/unindent) on each line at the first non-whitespace
+ // character, skipping lines that are either empty or just whitespace.
+ // This method is used by Indent, Unindent, IncreaseLineIndent, and DecreaseLineIndent, and
+ // is essentially a replacement for the editor primitive's Indent and Unindent functions.
+ private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, bool> action)
+ {
+ Func<ITextEdit, bool> editAction = edit =>
+ {
+ ITextSnapshot snapshot = _textView.TextSnapshot;
+
+ int startLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.Start.Position);
+ int endLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.End.Position);
+
+ for (int i = startLineNumber; i <= endLineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+
+ // If the line is blank or the (non-empty) selection ends at the start of this line, exclude
+ // the line from processing.
+ if (line.Length == 0 ||
+ (!_textView.Selection.IsEmpty && line.Start == _textView.Selection.End.Position))
+ continue;
+
+ TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(line.Start).GetFirstNonWhiteSpaceCharacterOnLine();
+
+ if (textPoint.CurrentPosition == textPoint.EndOfLine)
+ continue;
+
+ SnapshotPoint point = new SnapshotPoint(snapshot, textPoint.CurrentPosition);
+
+ if (!action(point, edit))
+ return false;
+ }
+
+ return true;
+ };
+
+ // Track the start of the selection with PointTrackingMode.Negative through the text
+ // change. If the result has the same absolute snapshot offset as the point before the change, we'll move
+ // the selection start point back to there instead of letting it track automatically.
+ ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(_textView.Selection.Start.Position, PointTrackingMode.Negative);
+ int startPositionBufferChange = _textView.Selection.Start.Position;
+
+ if (!EditHelper(editAction))
+ return false;
+
+ VirtualSnapshotPoint newStart = new VirtualSnapshotPoint(startPoint.GetPoint(_textView.TextSnapshot));
+ if (newStart.Position == startPositionBufferChange)
+ {
+ bool isReversed = _textView.Selection.IsReversed;
+ VirtualSnapshotPoint anchor = isReversed ? _textView.Selection.End : newStart;
+ VirtualSnapshotPoint active = isReversed ? newStart : _textView.Selection.End;
+ SelectAndMoveCaret(anchor, active);
+ }
+
+ return true;
+ }
+
+ // This is used by indent/unindent should be multiline operations. To be multiline, the selection
+ // points must be on separate lines, and not just an entire line (though not the entire last line, which
+ // we special case). This is for backwards compatibility with Orcas, but is generally undesirable behavior.
+ // Dev10 #856382 tracks removing this special behavior for indent and just treating it like a tab at the
+ // start of the selection.
+ private bool IndentOperationShouldBeMultiLine
+ {
+ get
+ {
+ if (_textView.Selection.IsEmpty)
+ return false;
+
+ var startLine = _textView.Selection.Start.Position.GetContainingLine();
+
+ bool pointsOnSameLine = _textView.Selection.End.Position <= startLine.End;
+
+ bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak;
+ bool entireLastLineSelected = lastLineOfFile &&
+ _textView.Selection.Start.Position == startLine.Start &&
+ _textView.Selection.End.Position == startLine.End;
+
+ return !pointsOnSameLine || entireLastLineSelected;
+ }
+ }
+
+ private bool InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ {
+ VirtualSnapshotPoint virtualPoint = new VirtualSnapshotPoint(point);
+ VirtualSnapshotSpan span = new VirtualSnapshotSpan(virtualPoint, virtualPoint);
+ return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true);
+ }
+
+ private bool InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false)
+ {
+ int indentSize = _textView.Options.GetIndentSize();
+ bool convertTabsToSpaces = _textView.Options.IsConvertTabsToSpacesEnabled();
+ bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
+ _textView.Selection.Start != _textView.Selection.End;
+ ITextSnapshot snapshot = edit.Snapshot;
+
+ VirtualSnapshotPoint point = span.Start;
+
+ // In a box selection, we don't insert anything for lines in virtual space
+ if (boxSelection && point.IsInVirtualSpace)
+ {
+ return true;
+ }
+
+ string textToInsert;
+
+ int startPointForReplace = point.Position;
+ int endPointForReplace = boxSelection ? startPointForReplace : span.End.Position;
+
+ int currentColumn;
+ int distanceToNextIndentStop;
+
+ if (!convertTabsToSpaces)
+ {
+ // First, move to the left to find the first non-space character
+ for (; startPointForReplace > 0; startPointForReplace--)
+ {
+ if (snapshot[startPointForReplace - 1] != ' ')
+ break;
+ }
+
+ // Find the column of the position before the spaces
+ TextPoint textPoint;
+ if (useBufferPrimitives)
+ textPoint = _editorPrimitives.Buffer.GetTextPoint(startPointForReplace);
+ else
+ textPoint = _editorPrimitives.View.GetTextPoint(startPointForReplace);
+
+ int startColumn = textPoint.Column;
+ currentColumn = startColumn + (point.Position - startPointForReplace) + point.VirtualSpaces;
+
+ if (exactlyOneIndentLevel)
+ distanceToNextIndentStop = indentSize;
+ else
+ distanceToNextIndentStop = indentSize - (currentColumn % indentSize);
+
+ int columnToInsertTo = currentColumn + distanceToNextIndentStop;
+ int totalDistance = columnToInsertTo - startColumn;
+
+ textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(new SnapshotPoint(snapshot, startPointForReplace), totalDistance, useBufferPrimitives);
+ }
+ else
+ {
+ // Here, we know we don't need to eat spaces
+ TextPoint textPoint;
+ if (useBufferPrimitives)
+ textPoint = _editorPrimitives.Buffer.GetTextPoint(point.Position.Position);
+ else
+ textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position);
+
+ currentColumn = textPoint.Column + point.VirtualSpaces;
+
+ if (exactlyOneIndentLevel)
+ distanceToNextIndentStop = indentSize;
+ else
+ distanceToNextIndentStop = indentSize - (currentColumn % indentSize);
+
+ int columnToInsertTo = currentColumn + distanceToNextIndentStop;
+
+ textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces + distanceToNextIndentStop, useBufferPrimitives);
+ }
+
+ if (_textView.Caret.OverwriteMode && span.IsEmpty)
+ {
+ // Walk forward, towards the end of the line, until we get to the column we will be
+ // overwriting.
+ int columnToInsertTo = currentColumn + distanceToNextIndentStop;
+
+ int lineEnd = point.Position.GetContainingLine().End;
+
+ for (; endPointForReplace < lineEnd; endPointForReplace++)
+ {
+ if (_editorPrimitives.View.GetTextPoint(endPointForReplace).Column >= columnToInsertTo)
+ break;
+ }
+ }
+
+ return edit.Replace(Span.FromBounds(startPointForReplace, endPointForReplace), textToInsert);
+ }
+
+ private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ {
+ return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true);
+ }
+
+ /// <summary>
+ /// Remove an "indent" to the left of the given point. If <paramref name="columnsToRemove"/> is specified, remove exactly
+ /// that amount of indent, instead of the normal logic of finding the previous indent stop.
+ /// </summary>
+ private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit, bool failOnNonWhitespaceCharacter, bool useBufferPrimitives = false, int? columnsToRemove = null)
+ {
+ // If we've been asked to remove *no* whitespace, then we're done.
+ if (columnsToRemove.HasValue && columnsToRemove.Value == 0)
+ return true;
+
+ // The logic for removing an indent is:
+ // 1) Find the previous indent level from the span's start position (or use columnsToRemove, if specified).
+ // 2) Delete characters back to the previous indent level, but
+ // 3) If the deletion goes past the previous indent level (i.e. by deleting a tab), insert spaces to bring the text
+ // back to the correct indent level.
+
+ ITextSnapshot snapshot = edit.Snapshot;
+
+ int indentSize = _editorOptions.GetIndentSize();
+
+ TextPoint startPointAsTextPoint;
+ if (useBufferPrimitives)
+ startPointAsTextPoint = _editorPrimitives.Buffer.GetTextPoint(point);
+ else
+ startPointAsTextPoint = _editorPrimitives.View.GetTextPoint(point);
+
+ int currentColumn = startPointAsTextPoint.Column;
+ int startOfLine = startPointAsTextPoint.StartOfLine;
+
+ if (startPointAsTextPoint.CurrentPosition == startOfLine)
+ return true;
+
+ // Determine the previous indent level; if columnsToRemove is specified, just subtract that
+ // from our current column. Otherwise, find the previous indent stop.
+ int previousIndentLevel;
+ if (columnsToRemove.HasValue)
+ previousIndentLevel = Math.Max(0, currentColumn - columnsToRemove.Value);
+ else
+ previousIndentLevel = ((currentColumn - 1) / indentSize) * indentSize;
+
+ int startPointForReplace = point.Position - 1;
+ int columnDeletedTo = 0;
+
+ // First, move to the left to find the first non-space character
+ for (; startPointForReplace >= startOfLine; startPointForReplace--)
+ {
+ // First, see if we *can* delete the character
+ char c = snapshot[startPointForReplace];
+ if (c != ' ' && c != '\t')
+ {
+ // We can't delete this character, so move forward
+ startPointForReplace++;
+ break;
+ }
+
+ // Now, see if we *should* delete the character
+ if (useBufferPrimitives)
+ columnDeletedTo = _editorPrimitives.Buffer.GetTextPoint(startPointForReplace).Column;
+ else
+ columnDeletedTo = _editorPrimitives.View.GetTextPoint(startPointForReplace).Column;
+
+ if (columnDeletedTo <= previousIndentLevel)
+ break;
+ }
+
+ // If the start point is at or past the span start, there isn't anything to replace.
+ if (startPointForReplace >= point)
+ return true;
+
+ // If we can't delete the full indent level and we've been asked to fail in this case, return false.
+ if (failOnNonWhitespaceCharacter && columnDeletedTo > previousIndentLevel)
+ return false;
+
+ string textToInsert = string.Empty;
+
+ if (columnDeletedTo < previousIndentLevel)
+ textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(new SnapshotPoint(snapshot, startPointForReplace), previousIndentLevel - columnDeletedTo, useBufferPrimitives);
+
+ return edit.Replace(Span.FromBounds(startPointForReplace, point.Position), textToInsert);
+ }
+
+ private void MoveCaretToPreviousIndentStopInVirtualSpace()
+ {
+ Debug.Assert(_textView.Caret.InVirtualSpace);
+
+ VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(_textView.Caret.Position.VirtualBufferPosition);
+ _textView.Caret.MoveTo(point);
+ }
+
+ /// <summary>
+ /// Used by the un-indenting logic to determine what an unindent means in virtual space.
+ /// </summary>
+ private VirtualSnapshotPoint GetPreviousIndentStopInVirtualSpace(VirtualSnapshotPoint point)
+ {
+ Debug.Assert(point.IsInVirtualSpace);
+
+ // If the point is in virtual space, then just move to the previous indent stop
+ // NOTE: this is a slight change in behavior from VS9. In VS9, the previous indent stop in
+ // virtual space would move the caret a indent size worth of characters to the left.
+ TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(point.Position);
+ int endOfLineColumn = textPoint.Column;
+ int column = endOfLineColumn + point.VirtualSpaces;
+ int indentSize = _editorOptions.GetIndentSize();
+ int previousIndentSize = ((column - 1) / indentSize) * indentSize;
+
+ if (previousIndentSize > endOfLineColumn)
+ return new VirtualSnapshotPoint(point.Position, previousIndentSize - endOfLineColumn);
+ else
+ return new VirtualSnapshotPoint(point.Position);
+ }
+
+ #endregion
+
+ #region Box Selection indent/unindent helpers
+
+ /// <summary>
+ /// Given a "fix-up" anchor/active point determined before the box operation, fix up the current selection's
+ /// anchor/active point. Either/both points may be null, if no correction is necessary.
+ /// </summary>
+ private void FixUpSelectionAfterBoxOperation(VirtualSnapshotPoint? anchorPoint, VirtualSnapshotPoint? activePoint)
+ {
+ if (anchorPoint.HasValue || activePoint.HasValue)
+ {
+ VirtualSnapshotPoint newAnchor = anchorPoint.HasValue ? anchorPoint.Value.TranslateTo(_textView.TextSnapshot) :
+ _textView.Selection.AnchorPoint;
+ VirtualSnapshotPoint newActive = activePoint.HasValue ? activePoint.Value.TranslateTo(_textView.TextSnapshot) :
+ _textView.Selection.ActivePoint;
+ SelectAndMoveCaret(newAnchor, newActive, TextSelectionMode.Box, EnsureSpanVisibleOptions.None);
+ }
+ }
+
+ /// <summary>
+ /// If a selection point (anchor or active) is in virtual space, it will need to move after an unindent
+ /// operation to leave the selection in a proper box.
+ /// </summary>
+ /// <returns>The new point to use after the unindent operation, or <c>null</c> if no special move logic
+ /// will be required.</returns>
+ private VirtualSnapshotPoint? CalculateBoxUnindentForSelectionPoint(VirtualSnapshotPoint point, int unindentAmount)
+ {
+ if (!point.IsInVirtualSpace)
+ return null;
+
+ var containingLine = _textView.GetTextViewLineContainingBufferPosition(point.Position);
+ var selectionOnLine = _textView.Selection.GetSelectionOnTextViewLine(containingLine);
+
+ // If the selection on this line isn't in virtual space, it'll track correctly
+ if (!selectionOnLine.HasValue || !selectionOnLine.Value.Start.IsInVirtualSpace)
+ return null;
+
+ int spaces = Math.Max(0, point.VirtualSpaces - unindentAmount);
+ return new VirtualSnapshotPoint(point.Position, spaces);
+ }
+
+ /// <summary>
+ /// If a selection point (anchor or active) is in virtual space, it will need to move after an indent
+ /// operation to leave the selection in a proper box.
+ /// </summary>
+ /// <returns>The new point to use after the indent operation, or <c>null</c> if no special move logic
+ /// will be required.</returns>
+ private VirtualSnapshotPoint? CalculateBoxIndentForSelectionPoint(VirtualSnapshotPoint point, int indentSize)
+ {
+ if (!point.IsInVirtualSpace)
+ return null;
+
+ TextPoint textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position);
+ int column = textPoint.Column + point.VirtualSpaces;
+ int distanceToNextIndentStop = indentSize - (column % indentSize);
+
+ return new VirtualSnapshotPoint(point.Position, point.VirtualSpaces + distanceToNextIndentStop);
+ }
+
+ /// <summary>
+ /// Determine the maximum amount the (current) box selection can be unindented, in columns, so that it will continue
+ /// to be a box after the indent operation.
+ /// </summary>
+ private int DetermineMaxBoxUnindent()
+ {
+ // We need to determine the correct amount of whitespace that can be unindented from each line.
+ // The amount removed should never be larger than the size of an indent, but it should also be consistent,
+ // meaning that the the smallest amount of whitespace to remove on any line becomes the amount removed
+ // from every line.
+ //
+ // This is *different* from how Orcas/Dev10RTM computed box unindent. These special cased much
+ // of the logic around moving an indent into the middle of a tab character, which we can now
+ // correctly handle.
+ int tabSize = _editorOptions.GetTabSize();
+
+ // The most we can indent, to start with, is an indentSize worth of indentation
+ int maxColumnUnindent = _editorOptions.GetIndentSize();
+
+ foreach (var point in _textView.Selection.VirtualSelectedSpans.Select(s => s.Start))
+ {
+ int columnsToAccountFor = maxColumnUnindent - point.VirtualSpaces;
+
+ if (columnsToAccountFor <= 0)
+ continue;
+
+ TextPoint textPoint = _editorPrimitives.View.GetTextPoint(point.Position);
+ int column = textPoint.Column;
+
+ // Walk the line backwards until we run out of columns to account for whitespace
+ // Note that targetColumn may be negative.
+ int targetColumn = column - columnsToAccountFor;
+
+ int startOfLine = textPoint.StartOfLine;
+
+ for (int i = point.Position.Position - 1; i >= startOfLine && textPoint.Column >= targetColumn; i--)
+ {
+ textPoint.MoveTo(i);
+ string character = textPoint.GetNextCharacter();
+ if (character != " " && character != "\t")
+ break;
+
+ column = textPoint.Column;
+ columnsToAccountFor = Math.Max(0, (column - targetColumn));
+
+ if (column <= targetColumn)
+ break;
+ }
+
+ maxColumnUnindent -= columnsToAccountFor;
+ }
+
+ Debug.Assert(maxColumnUnindent <= _editorOptions.GetIndentSize());
+ return maxColumnUnindent;
+ }
+
+ #endregion
+
+ #region Miscellaneous line helpers
+
+ private DisplayTextRange GetFullLines()
+ {
+ DisplayTextRange selectionRange = _editorPrimitives.Selection.Clone();
+ DisplayTextPoint startPoint = selectionRange.GetDisplayStartPoint();
+ DisplayTextPoint endPoint = selectionRange.GetDisplayEndPoint();
+ startPoint.MoveTo(startPoint.StartOfViewLine);
+ endPoint.MoveToBeginningOfNextViewLine();
+
+ return startPoint.GetDisplayTextRange(endPoint);
+ }
+
+ /// <summary>
+ /// Get the fully visible text line.
+ /// </summary>
+ /// <param name="textLine">The first text line to check.</param>
+ /// <param name="indexOfNextLine">The index of the next text line to check</param>
+ /// <returns>
+ /// The text line passed in if it is fully visible, there is only one text line, or the next text line is not fully visible.
+ /// Otherwise returns the text line at indexOfNextTextLine.
+ /// </returns>
+ private ITextViewLine FindFullyVisibleLine(ITextViewLine textLine, int indexOfNextLine)
+ {
+ if (textLine.VisibilityState != VisibilityState.FullyVisible)
+ {
+ if ((indexOfNextLine >= 0) && (_editorPrimitives.View.AdvancedTextView.TextViewLines.Count > indexOfNextLine))
+ {
+ if (_editorPrimitives.View.AdvancedTextView.TextViewLines[indexOfNextLine].VisibilityState == VisibilityState.FullyVisible)
+ {
+ return _editorPrimitives.View.AdvancedTextView.TextViewLines[indexOfNextLine];
+ }
+ }
+ }
+
+ return textLine;
+ }
+
+ private void MoveCaretToTextLine(ITextViewLine textLine, bool select)
+ {
+ VirtualSnapshotPoint anchor = _textView.Selection.AnchorPoint;
+ _textView.Caret.MoveTo(textLine);
+ if (select)
+ {
+ _textView.Selection.Select(anchor.TranslateTo(_textView.TextSnapshot), _textView.Caret.Position.VirtualBufferPosition);
+ }
+ else
+ {
+ _textView.Selection.Clear();
+ }
+ }
+
+ static bool IsPointOnBlankLine(TextPoint textPoint)
+ {
+ TextPoint firstTextColumn = textPoint.GetFirstNonWhiteSpaceCharacterOnLine();
+ return firstTextColumn.CurrentPosition == textPoint.EndOfLine;
+ }
+
+ static bool IsPointOnBlankViewLine(DisplayTextPoint displayTextPoint)
+ {
+ DisplayTextPoint firstTextColumn = displayTextPoint.GetFirstNonWhiteSpaceCharacterOnViewLine();
+ return firstTextColumn.CurrentPosition == displayTextPoint.EndOfViewLine;
+ }
+
+ #endregion
+
+ #region Tabs <-> spaces
+
+ private bool ConvertSpacesAndTabsHelper(bool toTabs)
+ {
+ bool wasEmpty = true;
+ bool wasReversed = false;
+ SnapshotSpan conversionRange;
+ if (_textView.Selection.IsEmpty)
+ {
+ conversionRange = _textView.Caret.ContainingTextViewLine.Extent; //This could contain multiple lines due to elisions.
+ }
+ else
+ {
+ conversionRange = _textView.Selection.StreamSelectionSpan.SnapshotSpan;
+ wasReversed = _textView.Selection.IsReversed;
+ wasEmpty = false;
+ }
+
+ using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit())
+ {
+ SnapshotPoint currentPosition = conversionRange.Start;
+ int tabSize = _editorOptions.GetTabSize();
+
+ while (currentPosition < conversionRange.End.Position)
+ {
+ //Do the conversion a line at a time since we are using the underlying ITextSnapshotLines to determin column position.
+ ITextSnapshotLine line = currentPosition.GetContainingLine();
+
+ int startOfConversion = Math.Max(line.Start.Position, conversionRange.Start.Position);
+ int endOfConversion = Math.Min(line.End.Position, conversionRange.End.Position);
+
+ //Get a TextPoint so we can use the editor primitives logic for handling multicharacter sequences, etc.
+ TextPoint point = _editorPrimitives.Buffer.GetTextPoint(startOfConversion);
+
+ int column = point.Column;
+
+ int whiteSpaceStart = point.CurrentPosition;
+ int whiteSpaceColumnStart = column;
+
+ while (point.CurrentPosition < endOfConversion)
+ {
+ char c = textEdit.Snapshot[point.CurrentPosition];
+
+ if (c == '\t')
+ {
+ column = ((column / tabSize) + 1) * tabSize;
+ }
+ else
+ {
+ if (c != ' ')
+ {
+ if (!ConvertTabsAndSpaces(textEdit, toTabs, tabSize, whiteSpaceStart, whiteSpaceColumnStart, point.CurrentPosition, column))
+ return false;
+ }
+ ++column;
+ }
+
+ //This handles multicharacter glyphs (at least to the degree that the TextPoint primitive understands them). We want this instead of DisplayTextPoint
+ //since we do not want this conversion to be affected by elisions or word wrap.
+ point.MoveToNextCharacter();
+
+ if ((c != ' ') && (c != '\t'))
+ {
+ whiteSpaceStart = point.CurrentPosition;
+ whiteSpaceColumnStart = column;
+ }
+ }
+
+ if (!ConvertTabsAndSpaces(textEdit, toTabs, tabSize, whiteSpaceStart, whiteSpaceColumnStart, point.CurrentPosition, column))
+ return false;
+
+ currentPosition = line.EndIncludingLineBreak;
+ }
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+ }
+
+ if (wasEmpty)
+ {
+ _textView.Caret.MoveTo(conversionRange.Start.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Negative));
+ }
+ else
+ {
+ _textView.Selection.Select(conversionRange.TranslateTo(_textView.TextSnapshot, SpanTrackingMode.EdgeInclusive), wasReversed);
+ _textView.Caret.MoveTo(_textView.Selection.ActivePoint);
+ }
+
+ return true;
+ }
+
+ private static bool ConvertTabsAndSpaces(ITextEdit textEdit, bool toTabs, int tabSize, int whiteSpaceStart, int whiteSpaceColumnStart, int whiteSpaceEnd, int whiteSpaceColumnEnd)
+ {
+ if (whiteSpaceEnd > whiteSpaceStart)
+ {
+ Debug.Assert(whiteSpaceColumnEnd > whiteSpaceColumnStart);
+
+ string textToInsert;
+ if (toTabs)
+ {
+ int spacesAfterPreviousTabStop = whiteSpaceColumnEnd % tabSize;
+
+ int columnOfPreviousTabStop = whiteSpaceColumnEnd - spacesAfterPreviousTabStop;
+
+ int requiredTabs = ((columnOfPreviousTabStop - whiteSpaceColumnStart) + tabSize - 1) / tabSize;
+ if (requiredTabs > 0)
+ textToInsert = new string('\t', requiredTabs) + new string(' ', spacesAfterPreviousTabStop);
+ else
+ textToInsert = new string(' ', whiteSpaceColumnEnd - whiteSpaceColumnStart);
+ }
+ else
+ {
+ textToInsert = new string(' ', whiteSpaceColumnEnd - whiteSpaceColumnStart);
+ }
+
+ Span replaceSpan = Span.FromBounds(whiteSpaceStart, whiteSpaceEnd);
+ if ((replaceSpan.Length != textToInsert.Length) || (textToInsert != textEdit.Snapshot.GetText(replaceSpan))) //performance hack: don't get the text if we know they'll be different.
+ return textEdit.Replace(replaceSpan, textToInsert);
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region Edit/Replace/Delete helpers
+
+ internal bool EditHelper(Func<ITextEdit, bool> editAction)
+ {
+ using (var edit = _textView.TextBuffer.CreateEdit())
+ {
+ if (!editAction(edit))
+ return false;
+ edit.Apply();
+ return !edit.Canceled;
+ }
+ }
+
+ internal bool ReplaceHelper(Span span, string text)
+ {
+ return EditHelper(e => e.Replace(span, text));
+ }
+
+ internal bool ReplaceHelper(VirtualSnapshotSpan virtualSnapshotSpan, string text)
+ {
+ if (virtualSnapshotSpan.Start.IsInVirtualSpace)
+ text = GetWhitespaceForVirtualSpace(virtualSnapshotSpan.Start) + text;
+ return EditHelper(e => e.Replace(virtualSnapshotSpan.SnapshotSpan, text));
+ }
+
+ internal bool ReplaceHelper(NormalizedSpanCollection spans, string text)
+ {
+ return EditHelper((e) =>
+ {
+ foreach (var span in spans)
+ {
+ if (!e.Replace(span, text) || e.Canceled)
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ internal bool ReplaceHelper(IEnumerable<VirtualSnapshotSpan> spans, string text)
+ {
+ return EditHelper((e) =>
+ {
+ foreach (var span in spans)
+ {
+ string lineText = text;
+
+ if (span.Start.IsInVirtualSpace)
+ lineText = GetWhitespaceForVirtualSpace(span.Start) + text;
+
+ if (!e.Replace(span.SnapshotSpan, lineText) || e.Canceled)
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ internal bool DeleteHelper(Span span)
+ {
+ return EditHelper(e => e.Delete(span));
+ }
+
+ internal bool DeleteHelper(NormalizedSpanCollection spans)
+ {
+ return EditHelper((e) =>
+ {
+ foreach (var span in spans)
+ {
+ if (!e.Delete(span) || e.Canceled)
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ #endregion
+
+ internal bool IsEmptyBoxSelection()
+ {
+ return !_textView.Selection.IsEmpty &&
+ _textView.Selection.VirtualSelectedSpans.All(s => s.IsEmpty);
+ }
+
+ /// <summary>
+ /// Position the caret using the smart indent service. Optionally, the caller can choose between
+ /// using only virtual space (and failing if that isn't possible) or using real spaces when the
+ /// line is not empty.
+ /// </summary>
+ /// <param name="useOnlyVirtualSpace">If <c>true</c>, don't ever use real spaces to position the caret,
+ /// even if there is no other way to position it with smart indent. Defaults to <c>true</c>.</param>
+ /// <param name="extendSelection">If <c>true</c>, extend the current selection, from the existing anchor point,
+ /// to the new caret position.</param>
+ /// <returns><c>true</c> if the caret was positioned in virtual space.</returns>
+ private bool PositionCaretWithSmartIndent(bool useOnlyVirtualSpace = true, bool extendSelection = false)
+ {
+ var caretPosition = _textView.Caret.Position.VirtualBufferPosition;
+ var caretLine = caretPosition.Position.GetContainingLine();
+
+ int? indentation = _factory.SmartIndentationService.GetDesiredIndentation(_textView, caretLine);
+ if (indentation.HasValue)
+ {
+ if (caretPosition.Position == caretLine.End)
+ {
+ //Position the caret in virtual space at the appropriate indentation.
+ var newCaretPoint = new VirtualSnapshotPoint(caretPosition.Position, Math.Max(0, indentation.Value - caretLine.Length));
+ var anchorPoint = (extendSelection) ? _textView.Selection.AnchorPoint : newCaretPoint;
+ SelectAndMoveCaret(anchorPoint, newCaretPoint, selectionMode: TextSelectionMode.Stream, scrollOptions: null);
+ return true;
+ }
+ else if (!useOnlyVirtualSpace)
+ {
+ // See how much whitespace already exists in the new line
+ int existingWhitespaceChars = GetLeadingWhitespaceChars(caretLine, caretPosition.Position);
+
+ // Insert the appropriate amount of leading whitespace, potentially replacing existing whitespace
+ string whitespace = GetWhiteSpaceForPositionAndVirtualSpace(caretPosition.Position, indentation.Value);
+ return ReplaceHelper(new SnapshotSpan(caretPosition.Position, existingWhitespaceChars), whitespace);
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns number of whitespace characters
+ /// between startPosition and first non-whitespace character in the specified line
+ /// </summary>
+ /// <param name="line">Which line to evaluate</param>
+ /// <param name="startPosition">Position where the count starts</param>
+ /// <returns>Number of leading whitespace characters located after startPosition</returns>
+ private int GetLeadingWhitespaceChars(ITextSnapshotLine line, SnapshotPoint startPosition)
+ {
+ int whitespace = 0;
+ for (int i = startPosition.Position; i < line.End; ++i)
+ {
+ if (IsSpaceCharacter(line.Snapshot[i]))
+ {
+ whitespace++;
+ }
+ else
+ break;
+ }
+ return whitespace;
+ }
+
+ private bool ConvertLeadingWhitespace(string actionName, bool convertTabsToSpaces)
+ {
+ Func<bool> action = () =>
+ {
+ using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = textEdit.Snapshot;
+
+ for (int i = _editorPrimitives.Selection.GetStartPoint().LineNumber; i <= _editorPrimitives.Selection.GetEndPoint().LineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+
+ TextPoint startOfLine = _editorPrimitives.Buffer.GetTextPoint(line.Start.Position);
+ TextPoint firstNonWhitespaceCharacter = startOfLine.GetFirstNonWhiteSpaceCharacterOnLine();
+ int columnOfFirstNonWhitespaceCharacter = firstNonWhitespaceCharacter.Column;
+
+ string whitespace = GetWhiteSpaceForPositionAndVirtualSpace(line.Start, columnOfFirstNonWhitespaceCharacter, /* useBufferPrimitives: */ true, convertTabsToSpaces);
+
+ SnapshotSpan currentWhiteSpace = new SnapshotSpan(line.Start, firstNonWhitespaceCharacter.AdvancedTextPoint);
+
+ if (whitespace != currentWhiteSpace.GetText())
+ {
+ if (!textEdit.Replace(currentWhiteSpace, whitespace))
+ return false;
+ }
+ }
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+ }
+
+ if (_editorPrimitives.Selection.IsEmpty)
+ {
+ _editorPrimitives.Caret.MoveTo(_editorPrimitives.Caret.StartOfLine);
+ }
+
+ return true;
+ };
+
+ return ExecuteAction(actionName, action);
+ }
+
+ /// <summary>
+ /// Expand a range to include the selection as well, if it is not empty, and return it.
+ /// </summary>
+ private TextRange ExpandRangeToIncludeSelection(TextRange range)
+ {
+ if (!_editorPrimitives.Selection.IsEmpty)
+ {
+ if (range.GetStartPoint().CurrentPosition > _editorPrimitives.Selection.GetStartPoint().CurrentPosition)
+ {
+ range.SetStart(_editorPrimitives.Selection.GetStartPoint());
+ }
+
+ if (range.GetEndPoint().CurrentPosition < _editorPrimitives.Selection.GetEndPoint().CurrentPosition)
+ {
+ range.SetEnd(_editorPrimitives.Selection.GetEndPoint());
+ }
+ }
+ return range;
+ }
+
+ private void ScrollByLineAndMoveCaretIfNecessary(ScrollDirection direction)
+ {
+ _textView.ViewScroller.ScrollViewportVerticallyByPixels(direction == ScrollDirection.Up ? _textView.LineHeight : (-_textView.LineHeight));
+
+ //If the caret is not on a fully visible line, move it to the first/last line in the view
+ ITextViewLine line = _textView.Caret.ContainingTextViewLine;
+ if (line.VisibilityState != VisibilityState.FullyVisible)
+ {
+ //Decide whether or not the caret is above the top or bottom of the view (since it doesn't lie
+ //on any of the fully visible lines in the middle of the view).
+ ITextViewLineCollection textLines = _textView.TextViewLines;
+ ITextViewLine newCaretLine;
+
+ //The end of the first line should be the start of the fully visible lines so use it to decide
+ //whether the caret is above or below.
+ ITextViewLine firstVisible = textLines.FirstVisibleLine;
+ if (_textView.Caret.Position.BufferPosition < firstVisible.EndIncludingLineBreak)
+ {
+ //The caret is above the top of the view, move it to the first fully visible line
+ //(or first partially visible if there is no fully visible line).
+ newCaretLine = firstVisible;
+ if (firstVisible.VisibilityState != VisibilityState.FullyVisible)
+ {
+ ITextViewLine nextLine = textLines.GetTextViewLineContainingBufferPosition(firstVisible.EndIncludingLineBreak);
+ if ((nextLine != null) && (nextLine.VisibilityState == VisibilityState.FullyVisible))
+ newCaretLine = nextLine;
+ }
+ }
+ else
+ {
+ //Otherwise the caret is below the bottom of the view. Move it to the last fully visible line.
+ ITextViewLine lastVisible = textLines.LastVisibleLine;
+ newCaretLine = lastVisible;
+
+ if ((lastVisible.VisibilityState != VisibilityState.FullyVisible) && (lastVisible.Start > 0))
+ {
+ ITextViewLine previousLine = textLines.GetTextViewLineContainingBufferPosition(lastVisible.Start - 1);
+ if ((previousLine != null) && (previousLine.VisibilityState == VisibilityState.FullyVisible))
+ newCaretLine = previousLine;
+ }
+ }
+
+ // Clear any selections
+ _textView.Selection.Clear();
+ _textView.Caret.MoveTo(newCaretLine);
+
+ //Since the caret is on a fully visible line, this should only scroll horizontally.
+ _textView.Caret.EnsureVisible();
+ }
+ }
+
+ /// <summary>
+ /// Determine if the character is a space, which includes everything in
+ /// the space separator category *plus* tab and a few others (for Orcas parity).
+ /// </summary>
+ private static bool IsSpaceCharacter(char c)
+ {
+ return c == ' ' || c == '\t' ||
+ (int)c == 0x200B ||
+ char.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
+ }
+
+ private bool ExecuteAction(string undoText, Func<bool> action, SelectionUpdate preserveCaretAndSelection = SelectionUpdate.Ignore, bool ensureVisible = false)
+ {
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(undoText))
+ {
+ ITextSnapshot snapshotBeforeEdit = _textView.TextSnapshot;
+
+ // 1. Before performing the action, add a Before undo primitive and remember the current
+ // state of the caret/selection
+ AddBeforeTextBufferChangePrimitive();
+
+ CaretPosition oldCaretPosition = _textView.Caret.Position;
+ VirtualSnapshotPoint oldSelectionAnchor = _textView.Selection.AnchorPoint;
+ VirtualSnapshotPoint oldSelectionActive = _textView.Selection.ActivePoint;
+
+ // 2. Perform the specified action
+ if (!action())
+ return false;
+
+ // 3. Take care of the requested SelectionUpdate
+ if (preserveCaretAndSelection == SelectionUpdate.Preserve)
+ {
+ _textView.Caret.MoveTo(new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot,
+ oldCaretPosition.BufferPosition.Position),
+ oldCaretPosition.VirtualSpaces), oldCaretPosition.Affinity);
+ _textView.Selection.Select(new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot,
+ oldSelectionAnchor.Position),
+ oldSelectionAnchor.VirtualSpaces),
+ new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot,
+ oldSelectionActive.Position),
+ oldSelectionActive.VirtualSpaces));
+ }
+ else if (preserveCaretAndSelection == SelectionUpdate.ClearVirtualSpace)
+ {
+ _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition);
+ _textView.Selection.Select(new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position),
+ new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position));
+ }
+ else if (preserveCaretAndSelection == SelectionUpdate.ResetUnlessEmptyBox)
+ {
+ if (!IsEmptyBoxSelection())
+ ResetSelection();
+ }
+ else if (preserveCaretAndSelection == SelectionUpdate.Reset)
+ {
+ ResetSelection();
+ }
+
+ // 4. If requested, make sure the caret is visible
+ if (ensureVisible)
+ _textView.Caret.EnsureVisible();
+
+ // 5. Finish up the undo transaction
+ AddAfterTextBufferChangePrimitive();
+
+ if (snapshotBeforeEdit != _textView.TextSnapshot)
+ {
+ undoTransaction.Complete();
+ }
+ }
+
+ return true;
+ }
+
+ private bool NormalizeLineEndingsHelper(string replacement)
+ {
+ // what do we do about projection? may wish to change this to navigate to the document buffer via
+ // text data model.
+ ITextBuffer textBuffer = _editorPrimitives.Buffer.AdvancedTextBuffer;
+ using (ITextEdit edit = textBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = edit.Snapshot;
+ foreach (ITextSnapshotLine line in snapshot.Lines)
+ {
+ if (line.LineBreakLength != 0)
+ {
+ string breakText = line.GetLineBreakText();
+ if (breakText != replacement)
+ {
+ if (!edit.Replace(line.End, line.LineBreakLength, replacement))
+ return false;
+ }
+ }
+ }
+ edit.Apply();
+ return !edit.Canceled;
+ }
+ }
+ }
+
+ /// <summary>
+ /// The maximum number of characters copied as rich text to the clipboard.
+ /// </summary>
+ /// <remarks>
+ /// Added as a local class here to avoid needing to patch text.logic (where this would, normally, go).
+ /// </remarks>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(MaxRtfCopyLength.OptionName)]
+ public sealed class MaxRtfCopyLength : EditorOptionDefinition<int>
+ {
+ public const string OptionName = "MaxRtfCopyLength";
+ public static readonly EditorOptionKey<int> OptionKey = new EditorOptionKey<int>(MaxRtfCopyLength.OptionName);
+
+ /// <summary>
+ /// Gets the default value (10K).
+ /// </summary>
+ public override int Default { get { return 10 * 1024; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return MaxRtfCopyLength.OptionKey; } }
+ }
+
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(UseAccurateClassificationForRtfCopy.OptionName)]
+ public sealed class UseAccurateClassificationForRtfCopy : EditorOptionDefinition<bool>
+ {
+ public const string OptionName = "UseAccurateClassificationForRtfCopy";
+ public static readonly EditorOptionKey<bool> OptionKey = new EditorOptionKey<bool>(UseAccurateClassificationForRtfCopy.OptionName);
+
+ public override bool Default { get { return false; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return UseAccurateClassificationForRtfCopy.OptionKey; } }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs
new file mode 100644
index 0000000..b18e07c
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs
@@ -0,0 +1,89 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Outlining;
+ using Microsoft.VisualStudio.Language.Intellisense.Utilities;
+
+ [Export(typeof(IEditorOperationsFactoryService))]
+ internal sealed class EditorOperationsFactoryService : IEditorOperationsFactoryService
+ {
+ [Import]
+ internal ITextStructureNavigatorSelectorService TextStructureNavigatorFactory { get; set; }
+
+ // This service should be optional: it is implemented on the VS side and other hosts may not implement it.
+ [Import(AllowDefault = true)]
+ internal IWaitIndicator WaitIndicator { get; set; }
+
+ [Import]
+ internal ITextSearchService TextSearchService { get; set; }
+
+ [Import]
+ internal ITextUndoHistoryRegistry UndoHistoryRegistry { get; set; }
+
+ [Import]
+ internal ITextBufferUndoManagerProvider TextBufferUndoManagerProvider { get; set; }
+
+ [Import]
+ internal IEditorPrimitivesFactoryService EditorPrimitivesProvider { get; set; }
+
+ [Import]
+ internal IEditorOptionsFactoryService EditorOptionsProvider { get; set; }
+
+ [Import]
+ internal IRtfBuilderService RtfBuilderService { get; set; }
+
+ [Import]
+ internal ISmartIndentationService SmartIndentationService { get; set; }
+
+ [Import]
+ internal ITextDocumentFactoryService TextDocumentFactoryService { get; set; }
+
+ [Import]
+ internal IContentTypeRegistryService ContentTypeRegistryService { get; set; }
+
+ [Import(AllowDefault = true)]
+ internal IOutliningManagerService OutliningManagerService { get; set; }
+
+ /// <summary>
+ /// Provides a operations implementation for a given text view.
+ /// </summary>
+ /// <param name="textView">
+ /// The text view to which the operations will be bound.
+ /// </param>
+ /// <returns>
+ /// An implementation of IEditorOperations that can provide operations implementations for the given text view.
+ /// </returns>
+ public IEditorOperations GetEditorOperations(ITextView textView)
+ {
+ // Validate
+ if (textView == null)
+ {
+ throw new ArgumentNullException("textView");
+ }
+
+ // Only one EditorOperations should be created per ITextView
+ IEditorOperations editorOperations = null;
+
+ // We create one, only if it doesn't already exist
+ if (!textView.Properties.TryGetProperty<IEditorOperations>(typeof(EditorOperationsFactoryService), out editorOperations))
+ {
+ editorOperations = new EditorOperations(textView, this);
+ textView.Properties.AddProperty(typeof(EditorOperationsFactoryService), editorOperations);
+ }
+
+ return editorOperations;
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/Strings.Designer.cs b/src/Text/Impl/EditorOperations/Strings.Designer.cs
new file mode 100644
index 0000000..a0b6e5d
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/Strings.Designer.cs
@@ -0,0 +1,486 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Text.Operations.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.UI.Text.EditorOperations.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot redo this change..
+ /// </summary>
+ internal static string CannotRedo {
+ get {
+ return ResourceManager.GetString("CannotRedo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot undo this change..
+ /// </summary>
+ internal static string CannotUndo {
+ get {
+ return ResourceManager.GetString("CannotUndo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Capitalize.
+ /// </summary>
+ internal static string Capitalize {
+ get {
+ return ResourceManager.GetString("Capitalize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Convert spaces to tabs.
+ /// </summary>
+ internal static string ConvertSpacesToTabs {
+ get {
+ return ResourceManager.GetString("ConvertSpacesToTabs", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Convert tabs to spaces.
+ /// </summary>
+ internal static string ConvertTabsToSpaces {
+ get {
+ return ResourceManager.GetString("ConvertTabsToSpaces", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cut line.
+ /// </summary>
+ internal static string CutLine {
+ get {
+ return ResourceManager.GetString("CutLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cut Selection.
+ /// </summary>
+ internal static string CutSelection {
+ get {
+ return ResourceManager.GetString("CutSelection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Decrease line indent.
+ /// </summary>
+ internal static string DecreaseLineIndent {
+ get {
+ return ResourceManager.GetString("DecreaseLineIndent", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete blank lines.
+ /// </summary>
+ internal static string DeleteBlankLines {
+ get {
+ return ResourceManager.GetString("DeleteBlankLines", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete Character To Left.
+ /// </summary>
+ internal static string DeleteCharToLeft {
+ get {
+ return ResourceManager.GetString("DeleteCharToLeft", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete Character To Right.
+ /// </summary>
+ internal static string DeleteCharToRight {
+ get {
+ return ResourceManager.GetString("DeleteCharToRight", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete horizontal white space.
+ /// </summary>
+ internal static string DeleteHorizontalWhiteSpace {
+ get {
+ return ResourceManager.GetString("DeleteHorizontalWhiteSpace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete line.
+ /// </summary>
+ internal static string DeleteLine {
+ get {
+ return ResourceManager.GetString("DeleteLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete Text.
+ /// </summary>
+ internal static string DeleteText {
+ get {
+ return ResourceManager.GetString("DeleteText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete to BOL.
+ /// </summary>
+ internal static string DeleteToBOL {
+ get {
+ return ResourceManager.GetString("DeleteToBOL", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete to EOL.
+ /// </summary>
+ internal static string DeleteToEOL {
+ get {
+ return ResourceManager.GetString("DeleteToEOL", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete Word To Left.
+ /// </summary>
+ internal static string DeleteWordToLeft {
+ get {
+ return ResourceManager.GetString("DeleteWordToLeft", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete Word To Right.
+ /// </summary>
+ internal static string DeleteWordToRight {
+ get {
+ return ResourceManager.GetString("DeleteWordToRight", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Increase line indent.
+ /// </summary>
+ internal static string IncreaseLineIndent {
+ get {
+ return ResourceManager.GetString("IncreaseLineIndent", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Indent Selection.
+ /// </summary>
+ internal static string IndentSelection {
+ get {
+ return ResourceManager.GetString("IndentSelection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert File.
+ /// </summary>
+ internal static string InsertFile {
+ get {
+ return ResourceManager.GetString("InsertFile", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert Final New Line.
+ /// </summary>
+ internal static string InsertFinalNewLine {
+ get {
+ return ResourceManager.GetString("InsertFinalNewLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert new line.
+ /// </summary>
+ internal static string InsertNewLine {
+ get {
+ return ResourceManager.GetString("InsertNewLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert Tab.
+ /// </summary>
+ internal static string InsertTab {
+ get {
+ return ResourceManager.GetString("InsertTab", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert Text.
+ /// </summary>
+ internal static string InsertText {
+ get {
+ return ResourceManager.GetString("InsertText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to MakeLowercase.
+ /// </summary>
+ internal static string MakeLowercase {
+ get {
+ return ResourceManager.GetString("MakeLowercase", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to MakeUppercase.
+ /// </summary>
+ internal static string MakeUppercase {
+ get {
+ return ResourceManager.GetString("MakeUppercase", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Move selected lines down.
+ /// </summary>
+ internal static string MoveSelLinesDown {
+ get {
+ return ResourceManager.GetString("MoveSelLinesDown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Move selected lines up.
+ /// </summary>
+ internal static string MoveSelLinesUp {
+ get {
+ return ResourceManager.GetString("MoveSelLinesUp", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Make Line Endings Consistent.
+ /// </summary>
+ internal static string NormalizeLineEndings {
+ get {
+ return ResourceManager.GetString("NormalizeLineEndings", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert new line above.
+ /// </summary>
+ internal static string OpenLineAbove {
+ get {
+ return ResourceManager.GetString("OpenLineAbove", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insert new line below.
+ /// </summary>
+ internal static string OpenLineBelow {
+ get {
+ return ResourceManager.GetString("OpenLineBelow", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Paste.
+ /// </summary>
+ internal static string Paste {
+ get {
+ return ResourceManager.GetString("Paste", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Remove Previous Tab.
+ /// </summary>
+ internal static string RemovePreviousTab {
+ get {
+ return ResourceManager.GetString("RemovePreviousTab", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Replace All.
+ /// </summary>
+ internal static string ReplaceAll {
+ get {
+ return ResourceManager.GetString("ReplaceAll", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Replace Selection With .
+ /// </summary>
+ internal static string ReplaceSelectionWith {
+ get {
+ return ResourceManager.GetString("ReplaceSelectionWith", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Replace Text.
+ /// </summary>
+ internal static string ReplaceText {
+ get {
+ return ResourceManager.GetString("ReplaceText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Tabify selection.
+ /// </summary>
+ internal static string Tabify {
+ get {
+ return ResourceManager.GetString("Tabify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Toggle case.
+ /// </summary>
+ internal static string ToggleCase {
+ get {
+ return ResourceManager.GetString("ToggleCase", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Transpose Character.
+ /// </summary>
+ internal static string TransposeCharacter {
+ get {
+ return ResourceManager.GetString("TransposeCharacter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Transpose Line.
+ /// </summary>
+ internal static string TransposeLine {
+ get {
+ return ResourceManager.GetString("TransposeLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Transpose word.
+ /// </summary>
+ internal static string TransposeWord {
+ get {
+ return ResourceManager.GetString("TransposeWord", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Trim trailing whitespace.
+ /// </summary>
+ internal static string TrimTrailingWhitespace {
+ get {
+ return ResourceManager.GetString("TrimTrailingWhitespace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unindent Selection.
+ /// </summary>
+ internal static string UnindentSelection {
+ get {
+ return ResourceManager.GetString("UnindentSelection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Untabify selection.
+ /// </summary>
+ internal static string Untabify {
+ get {
+ return ResourceManager.GetString("Untabify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Formatting selection.
+ /// </summary>
+ internal static string WaitMessage {
+ get {
+ return ResourceManager.GetString("WaitMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Copy.
+ /// </summary>
+ internal static string WaitTitle {
+ get {
+ return ResourceManager.GetString("WaitTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/Strings.resx b/src/Text/Impl/EditorOperations/Strings.resx
new file mode 100644
index 0000000..fd8f056
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/Strings.resx
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CannotUndo" xml:space="preserve">
+ <value>Cannot undo this change.</value>
+ </data>
+ <data name="CannotRedo" xml:space="preserve">
+ <value>Cannot redo this change.</value>
+ </data>
+ <data name="InsertNewLine" xml:space="preserve">
+ <value>Insert new line</value>
+ </data>
+ <data name="InsertTab" xml:space="preserve">
+ <value>Insert Tab</value>
+ </data>
+ <data name="RemovePreviousTab" xml:space="preserve">
+ <value>Remove Previous Tab</value>
+ </data>
+ <data name="IndentSelection" xml:space="preserve">
+ <value>Indent Selection</value>
+ </data>
+ <data name="UnindentSelection" xml:space="preserve">
+ <value>Unindent Selection</value>
+ </data>
+ <data name="InsertText" xml:space="preserve">
+ <value>Insert Text</value>
+ </data>
+ <data name="DeleteText" xml:space="preserve">
+ <value>Delete Text</value>
+ </data>
+ <data name="ReplaceSelectionWith" xml:space="preserve">
+ <value>Replace Selection With </value>
+ </data>
+ <data name="ReplaceText" xml:space="preserve">
+ <value>Replace Text</value>
+ </data>
+ <data name="ReplaceAll" xml:space="preserve">
+ <value>Replace All</value>
+ </data>
+ <data name="CutSelection" xml:space="preserve">
+ <value>Cut Selection</value>
+ </data>
+ <data name="Paste" xml:space="preserve">
+ <value>Paste</value>
+ </data>
+ <data name="TransposeCharacter" xml:space="preserve">
+ <value>Transpose Character</value>
+ </data>
+ <data name="TransposeLine" xml:space="preserve">
+ <value>Transpose Line</value>
+ </data>
+ <data name="MakeLowercase" xml:space="preserve">
+ <value>MakeLowercase</value>
+ </data>
+ <data name="MakeUppercase" xml:space="preserve">
+ <value>MakeUppercase</value>
+ </data>
+ <data name="DeleteWordToRight" xml:space="preserve">
+ <value>Delete Word To Right</value>
+ </data>
+ <data name="DeleteWordToLeft" xml:space="preserve">
+ <value>Delete Word To Left</value>
+ </data>
+ <data name="DeleteCharToLeft" xml:space="preserve">
+ <value>Delete Character To Left</value>
+ </data>
+ <data name="DeleteCharToRight" xml:space="preserve">
+ <value>Delete Character To Right</value>
+ </data>
+ <data name="InsertFile" xml:space="preserve">
+ <value>Insert File</value>
+ </data>
+ <data name="Capitalize" xml:space="preserve">
+ <value>Capitalize</value>
+ </data>
+ <data name="ToggleCase" xml:space="preserve">
+ <value>Toggle case</value>
+ </data>
+ <data name="DeleteLine" xml:space="preserve">
+ <value>Delete line</value>
+ </data>
+ <data name="CutLine" xml:space="preserve">
+ <value>Cut line</value>
+ </data>
+ <data name="DeleteToBOL" xml:space="preserve">
+ <value>Delete to BOL</value>
+ </data>
+ <data name="DeleteToEOL" xml:space="preserve">
+ <value>Delete to EOL</value>
+ </data>
+ <data name="OpenLineAbove" xml:space="preserve">
+ <value>Insert new line above</value>
+ </data>
+ <data name="OpenLineBelow" xml:space="preserve">
+ <value>Insert new line below</value>
+ </data>
+ <data name="TransposeWord" xml:space="preserve">
+ <value>Transpose word</value>
+ </data>
+ <data name="IncreaseLineIndent" xml:space="preserve">
+ <value>Increase line indent</value>
+ </data>
+ <data name="DecreaseLineIndent" xml:space="preserve">
+ <value>Decrease line indent</value>
+ </data>
+ <data name="DeleteBlankLines" xml:space="preserve">
+ <value>Delete blank lines</value>
+ </data>
+ <data name="DeleteHorizontalWhiteSpace" xml:space="preserve">
+ <value>Delete horizontal white space</value>
+ </data>
+ <data name="Tabify" xml:space="preserve">
+ <value>Tabify selection</value>
+ </data>
+ <data name="Untabify" xml:space="preserve">
+ <value>Untabify selection</value>
+ </data>
+ <data name="ConvertTabsToSpaces" xml:space="preserve">
+ <value>Convert tabs to spaces</value>
+ </data>
+ <data name="ConvertSpacesToTabs" xml:space="preserve">
+ <value>Convert spaces to tabs</value>
+ </data>
+ <data name="NormalizeLineEndings" xml:space="preserve">
+ <value>Make Line Endings Consistent</value>
+ </data>
+ <data name="MoveSelLinesUp" xml:space="preserve">
+ <value>Move selected lines up</value>
+ </data>
+ <data name="MoveSelLinesDown" xml:space="preserve">
+ <value>Move selected lines down</value>
+ </data>
+ <data name="WaitMessage" xml:space="preserve">
+ <value>Formatting selection</value>
+ </data>
+ <data name="WaitTitle" xml:space="preserve">
+ <value>Copy</value>
+ </data>
+ <data name="InsertFinalNewLine" xml:space="preserve">
+ <value>Insert Final New Line</value>
+ </data>
+ <data name="TrimTrailingWhitespace" xml:space="preserve">
+ <value>Trim trailing whitespace</value>
+ </data>
+</root>
diff --git a/src/Text/Impl/EditorOperations/TextEditAction.cs b/src/Text/Impl/EditorOperations/TextEditAction.cs
new file mode 100644
index 0000000..c5b3f54
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/TextEditAction.cs
@@ -0,0 +1,25 @@
+//
+// 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.Operations.Implementation
+{
+ /// <summary>
+ /// Enum value stating type of text edit action.
+ /// </summary>
+ internal enum TextEditAction
+ {
+ None,
+ Type,
+ Delete,
+ Backspace,
+ Paste,
+ Enter,
+ AutoIndent,
+ Replace,
+ ProvisionalOverwrite
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs
new file mode 100644
index 0000000..d913c0d
--- /dev/null
+++ b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs
@@ -0,0 +1,140 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Linq;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text;
+ using System.Diagnostics;
+
+ [Flags]
+ internal enum TextTransactionMergeDirections
+ {
+ Forward = 0x0001,
+ Backward = 0x0002
+ }
+
+ /// <summary>
+ /// This is the merge policy used for determining whether text's undo transactions can be merged.
+ /// </summary>
+ internal class TextTransactionMergePolicy : IMergeTextUndoTransactionPolicy
+ {
+ #region Private members
+ TextTransactionMergeDirections _allowableMergeDirections;
+ #endregion
+
+ #region Constructors
+ public TextTransactionMergePolicy() : this (TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward)
+ {
+ }
+
+ public TextTransactionMergePolicy(TextTransactionMergeDirections allowableMergeDirections)
+ {
+ _allowableMergeDirections = allowableMergeDirections;
+ }
+ #endregion
+
+ #region IMergeTextUndoTransactionPolicy Members
+
+ public bool CanMerge(ITextUndoTransaction newTransaction, ITextUndoTransaction oldTransaction)
+ {
+ // Validate
+ if (newTransaction == null)
+ {
+ throw new ArgumentNullException("newTransaction");
+ }
+
+ if (oldTransaction == null)
+ {
+ throw new ArgumentNullException("oldTransaction");
+ }
+
+ TextTransactionMergePolicy oldPolicy = oldTransaction.MergePolicy as TextTransactionMergePolicy;
+ TextTransactionMergePolicy newPolicy = newTransaction.MergePolicy as TextTransactionMergePolicy;
+ if (oldPolicy == null || newPolicy == null)
+ {
+ throw new InvalidOperationException("The MergePolicy for both transactions should be a TextTransactionMergePolicy.");
+ }
+
+ // Make sure the merge policy directions permit merging these two transactions.
+ if ((oldPolicy._allowableMergeDirections & TextTransactionMergeDirections.Forward) == 0 ||
+ (newPolicy._allowableMergeDirections & TextTransactionMergeDirections.Backward) == 0)
+ {
+ return false;
+ }
+
+ // Only merge text transactions that have the same description
+ if (newTransaction.Description != oldTransaction.Description)
+ {
+ return false;
+ }
+
+ // If one of the transactions is empty, than it is safe to merge
+ if ((newTransaction.UndoPrimitives.Count == 0) || (oldTransaction.UndoPrimitives.Count == 0))
+ {
+ return true;
+ }
+
+ // Make sure that we only merge consecutive edits
+ ITextUndoPrimitive newerBeforeTextChangePrimitive = newTransaction.UndoPrimitives[0];
+ ITextUndoPrimitive olderAfterTextChangePrimitive = oldTransaction.UndoPrimitives[oldTransaction.UndoPrimitives.Count - 1];
+
+ return newerBeforeTextChangePrimitive.CanMerge(olderAfterTextChangePrimitive);
+ }
+
+ public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction)
+ {
+ if (existingTransaction == null)
+ throw new ArgumentNullException("existingTransaction");
+ if (newTransaction == null)
+ throw new ArgumentNullException("newTransaction");
+
+ // Remove trailing AfterTextBufferChangeUndoPrimitive from previous transaction and skip copying
+ // initial BeforeTextBufferChangeUndoPrimitive from newTransaction, as they are unnecessary.
+ int copyStartIndex = 0;
+ int existingCount = existingTransaction.UndoPrimitives.Count;
+ int newCount = newTransaction.UndoPrimitives.Count;
+ if (existingCount > 0 &&
+ newCount > 0 &&
+ existingTransaction.UndoPrimitives[existingCount - 1] is AfterTextBufferChangeUndoPrimitive &&
+ newTransaction.UndoPrimitives[0] is BeforeTextBufferChangeUndoPrimitive)
+ {
+ existingTransaction.UndoPrimitives.RemoveAt(existingCount - 1);
+ copyStartIndex = 1;
+ }
+ else
+ {
+ // Unless undo is disabled (in which case both transactions will be empty), this is unexpected.
+ Debug.Assert(existingCount == 0 && newCount == 0,
+ "Expected previous transaction to end with AfterTextBufferChangeUndoPrimitive and "
+ + "new transaction to start with BeforeTextBufferChangeUndoPrimitive");
+ }
+
+ // Copy items from newTransaction into existingTransaction.
+ for (int i = copyStartIndex; i < newTransaction.UndoPrimitives.Count; i++)
+ {
+ existingTransaction.UndoPrimitives.Add(newTransaction.UndoPrimitives[i]);
+ }
+ }
+
+ public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException("other");
+ }
+
+ // Only merge transaction if they are both a text transaction
+ return this.GetType() == other.GetType();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorOptions/EditorOptions.cs b/src/Text/Impl/EditorOptions/EditorOptions.cs
new file mode 100644
index 0000000..59a6e55
--- /dev/null
+++ b/src/Text/Impl/EditorOptions/EditorOptions.cs
@@ -0,0 +1,360 @@
+//
+// 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.EditorOptions.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.Linq;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ internal class EditorOptions : IEditorOptions
+ {
+ IPropertyOwner Scope { get; set; }
+
+ HybridDictionary OptionsSetLocally { get; set; }
+
+ private EditorOptionsFactoryService _factory;
+
+ FrugalList<WeakReference> DerivedEditorOptions = new FrugalList<WeakReference>();
+
+ internal EditorOptions(EditorOptions parent,
+ IPropertyOwner scope,
+ EditorOptionsFactoryService factory)
+ {
+ _parent = parent;
+ _factory = factory;
+ this.Scope = scope;
+
+ this.OptionsSetLocally = new HybridDictionary();
+
+ if (parent != null)
+ {
+ parent.AddDerivedOptions(this);
+ }
+ }
+
+ #region IEditorOptions Members
+
+ private EditorOptions _parent;
+ public IEditorOptions Parent
+ {
+ get
+ {
+ return _parent;
+ }
+ set
+ {
+ if (_parent == value)
+ return;
+
+ // _parent == null => this is the global options instance
+ if (_parent == null)
+ throw new InvalidOperationException("Cannot change the Parent of the global options.");
+
+ if (value == null)
+ throw new ArgumentNullException("value");
+
+ if (value == this)
+ throw new ArgumentException("The Parent of this instance of IEditorOptions cannot be set to itself.");
+
+ EditorOptions newParent = value as EditorOptions;
+
+ if (newParent == null)
+ throw new ArgumentException("New parent must be an instance of IEditorOptions generated by the same factory as this instance.");
+
+ var oldParent = _parent;
+
+ _parent.RemovedDerivedOptions(this);
+ _parent = newParent;
+ _parent.AddDerivedOptions(this);
+
+ this.CheckForCycle();
+
+ // TODO: Should we be more specific? Should there be a
+ // version of OptionsChanged that says "everything has changed"?
+
+ // Send out an event for each supported option that isn't already
+ // set locally (since the update in parent won't change the
+ // observed value).
+ foreach (var definition in _factory.GetInstantiatedOptions(this.Scope))
+ {
+ if (!this.OptionsSetLocally.Contains(definition.Name))
+ {
+ object oldValue = oldParent.GetOptionForChild(definition);
+ object newValue = _parent.GetOptionForChild(definition);
+
+ if (!object.Equals(oldValue, newValue))
+ RaiseChangedEvent(definition);
+ }
+ }
+ }
+ }
+
+ public T GetOptionValue<T>(string optionId)
+ {
+ var definition = _factory.GetOptionDefinitionOrThrow(optionId);
+
+ if (!typeof(T).IsAssignableFrom(definition.ValueType))
+ throw new InvalidOperationException("Invalid type requested for the given option.");
+
+ object value = this.GetOptionValue(definition);
+ return (T)value;
+ }
+
+ public T GetOptionValue<T>(EditorOptionKey<T> key)
+ {
+ return GetOptionValue<T>(key.Name);
+ }
+
+ public object GetOptionValue(string optionId)
+ {
+ return GetOptionValue(_factory.GetOptionDefinitionOrThrow(optionId));
+ }
+
+ private object GetOptionValue(EditorOptionDefinition definition)
+ {
+ object value;
+
+ if (!TryGetOption(definition, out value))
+ throw new ArgumentException(string.Format("The specified option is not valid in this scope: {0}", definition.Name));
+
+ return value;
+ }
+
+ public void SetOptionValue(string optionId, object value)
+ {
+ EditorOptionDefinition definition = _factory.GetOptionDefinitionOrThrow(optionId);
+
+ // Make sure the type of the provided value is correct
+ if (!definition.ValueType.IsAssignableFrom(value.GetType()))
+ {
+ throw new ArgumentException("Specified option value is of an invalid type", "value");
+ }
+ // Make sure the option is valid, also
+ else if(!definition.IsValid(ref value))
+ {
+ throw new ArgumentException("The supplied value failed validation for the option.", "value");
+ }
+ // Finally, set the option value locally
+ else
+ {
+ object currentValue = this.GetOptionValue(definition);
+ OptionsSetLocally[optionId] = value;
+
+ if (!object.Equals(currentValue, value))
+ {
+ RaiseChangedEvent(definition);
+ }
+ }
+ }
+
+ public void SetOptionValue<T>(EditorOptionKey<T> key, T value)
+ {
+ SetOptionValue(key.Name, value);
+ }
+
+ public bool IsOptionDefined(string optionId, bool localScopeOnly)
+ {
+ if (localScopeOnly && (_parent != null)) //All options with valid definitions are set for the root.
+ return OptionsSetLocally.Contains(optionId);
+
+ EditorOptionDefinition definition = _factory.GetOptionDefinition(optionId);
+ if ((definition != null) &&
+ (Scope == null || definition.IsApplicableToScope(Scope)))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool IsOptionDefined<T>(EditorOptionKey<T> key, bool localScopeOnly)
+ {
+ if (localScopeOnly && (_parent != null)) //All options with valid definitions are set for the root.
+ {
+ return OptionsSetLocally.Contains(key.Name);
+ }
+
+ EditorOptionDefinition definition = _factory.GetOptionDefinition(key.Name);
+ if ((definition != null) &&
+ (Scope == null || definition.IsApplicableToScope(Scope)) &&
+ definition.ValueType.IsEquivalentTo(typeof(T)))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool ClearOptionValue(string optionId)
+ {
+ if (this.Parent == null)
+ {
+ // Can't clear options on the Global options
+ return false;
+ }
+
+ if (OptionsSetLocally.Contains(optionId))
+ {
+ object currentValue = OptionsSetLocally[optionId];
+
+ OptionsSetLocally.Remove(optionId);
+
+ EditorOptionDefinition definition = _factory.GetOptionDefinitionOrThrow(optionId);
+ object inheritedValue = this.GetOptionValue(definition);
+
+ // See what the inherited option value was. If it isn't changing,
+ // then we don't need to raise an event.
+ if (!object.Equals(currentValue, inheritedValue))
+ {
+ RaiseChangedEvent(definition);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool ClearOptionValue<T>(EditorOptionKey<T> key)
+ {
+ return ClearOptionValue(key.Name);
+ }
+
+ public IEnumerable<EditorOptionDefinition> SupportedOptions
+ {
+ get
+ {
+ return _factory.GetSupportedOptions(this.Scope);
+ }
+ }
+
+ public IEditorOptions GlobalOptions
+ {
+ get
+ {
+ return _factory.GlobalOptions;
+ }
+ }
+
+ public event EventHandler<EditorOptionChangedEventArgs> OptionChanged;
+
+ #endregion
+
+ #region Private Helpers
+
+ //A hook so we can tell whether or not the options have been hooked. Used only by unit tests.
+ private object OptionChangedValue { get { return this.OptionChanged; } }
+
+ private void RaiseChangedEvent(EditorOptionDefinition definition)
+ {
+ // First, send out local events, but only if the change is valid in this scope
+ if (Scope == null || definition.IsApplicableToScope(Scope))
+ {
+ var tempEvent = OptionChanged;
+ if (tempEvent != null)
+ tempEvent(this, new EditorOptionChangedEventArgs(definition.Name));
+ }
+
+ // Get rid of the expired refs
+ DerivedEditorOptions.RemoveAll(weakref => !weakref.IsAlive);
+
+ // Now, notify a copy of the derived options (since an event might modify the DerivedEditorOptions).
+ foreach (var weakRef in new FrugalList<WeakReference>(DerivedEditorOptions))
+ {
+ EditorOptions derived = weakRef.Target as EditorOptions;
+ if (derived != null)
+ derived.OnParentOptionChanged(definition);
+ }
+ }
+
+ private void CheckForCycle()
+ {
+ EditorOptions parent = _parent;
+ HashSet<EditorOptions> visited = new HashSet<EditorOptions>();
+
+ while (parent != null)
+ {
+ if (visited.Contains(parent))
+ throw new ArgumentException("Cycles are not allowed in the Parent chain.");
+
+ visited.Add(parent);
+ parent = parent._parent;
+ }
+ }
+
+ #endregion
+
+ #region Internal "event" handling
+
+ internal void AddDerivedOptions(EditorOptions derived)
+ {
+ // Get rid of the expired refs
+ DerivedEditorOptions.RemoveAll(weakref => !weakref.IsAlive);
+
+ DerivedEditorOptions.Add(new WeakReference(derived));
+ }
+
+ internal void RemovedDerivedOptions(EditorOptions derived)
+ {
+ foreach (var weakRef in DerivedEditorOptions)
+ {
+ if (weakRef.Target == derived)
+ {
+ DerivedEditorOptions.Remove(weakRef);
+ break;
+ }
+ }
+ }
+
+ internal void OnParentOptionChanged(EditorOptionDefinition definition)
+ {
+ // We only notify if the given option isn't already set locally, since it
+ // would be overriden by a parent option changing.
+ if (!this.OptionsSetLocally.Contains(definition.Name))
+ RaiseChangedEvent(definition);
+ }
+
+ #endregion
+
+ private bool TryGetOption(EditorOptionDefinition definition, out object value)
+ {
+ value = null;
+
+ if (Scope != null && !definition.IsApplicableToScope(Scope))
+ return false;
+
+ value = this.GetOptionForChild(definition);
+ return true;
+ }
+
+ /// <summary>
+ /// Get the given option from this (or its ancestors). The caller should have already checked to ensure
+ /// the given option is valid in the scope being requested.
+ /// </summary>
+ /// <param name="definition">Definition of the option to find.</param>
+ /// <returns>The option's current value.</returns>
+ internal object GetOptionForChild(EditorOptionDefinition definition)
+ {
+ if (OptionsSetLocally.Contains(definition.Name))
+ {
+ return OptionsSetLocally[definition.Name];
+ }
+
+ if (_parent == null)
+ {
+ return definition.DefaultValue;
+ }
+
+ return _parent.GetOptionForChild(definition);
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
new file mode 100644
index 0000000..733450b
--- /dev/null
+++ b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
@@ -0,0 +1,185 @@
+//
+// 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.EditorOptions.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.ComponentModel.Composition;
+ using System.Globalization;
+ using System.Linq;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ [Export(typeof(IEditorOptionsFactoryService))]
+ internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService
+ {
+ [ImportMany(typeof(EditorOptionDefinition))]
+ internal List<Lazy<EditorOptionDefinition, INameMetadata>> OptionImports { get; set; }
+
+ private EditorOptions _globalOptions;
+ private IDictionary<string, EditorOptionDefinition> _instantiatedOptionDefinitions = new Dictionary<string, EditorOptionDefinition>();
+ private IDictionary<string, Lazy<EditorOptionDefinition, INameMetadata>> _namedOptionImports = new Dictionary<string, Lazy<EditorOptionDefinition, INameMetadata>>();
+
+ [Import]
+ internal GuardedOperations guardedOperations = null;
+
+ #region IEditorOptionsFactoryService Members
+
+ public IEditorOptions GetOptions(IPropertyOwner scope)
+ {
+ if (scope == null)
+ throw new ArgumentNullException("scope");
+
+ return scope.Properties.GetOrCreateSingletonProperty<IEditorOptions>(() => new EditorOptions(this.GlobalOptions as EditorOptions, scope, this));
+ }
+
+ public IEditorOptions CreateOptions()
+ {
+ return new EditorOptions(this.GlobalOptions as EditorOptions, null, this);
+ }
+
+ public IEditorOptions GlobalOptions
+ {
+ get
+ {
+ if (_globalOptions == null)
+ {
+ //We're guranteed that the first thing that happens when anyone tries to create options is that the global options will be created first,
+ //so do initialization here.
+ _globalOptions = new EditorOptions(null, null, this);
+
+ //Initialize _after_ setting _globalOptions so that -- since this is a property -- we will only be initialized once if stepping through
+ //this code in the debugger (and trying to evaluate the .GlobalOptions in the watch window.
+ this.Initialize();
+ }
+
+ return _globalOptions;
+ }
+ }
+ #endregion
+
+ private void Initialize()
+ {
+ //Don't need to start locking things (yet)
+ foreach (var import in this.OptionImports)
+ {
+ if (import.Metadata.Name != null)
+ {
+ //The external user kindly provided a name as metadata.
+ this.SafeAdd(_namedOptionImports, import.Metadata.Name, import);
+
+#if DEBUG
+ if (import.Metadata.Name.Contains('\\'))
+ System.Diagnostics.Debug.WriteLine("option with \\ " + import.Metadata.Name);
+#endif
+ }
+ else
+ {
+ //They didn't so we need to instantiate the extension in order to discover the name.
+ var definition = this.guardedOperations.InstantiateExtension(import, import);
+
+ this.SafeAdd(_instantiatedOptionDefinitions, definition.Name, definition);
+
+
+#if DEBUG
+ System.Diagnostics.Debug.WriteLine("unnamed option: " + definition.Name);
+#endif
+ }
+ }
+ }
+
+ private void SafeAdd<T>(IDictionary<string, T> dictionary, string name, T value)
+ {
+ try
+ {
+ dictionary.Add(name, value);
+ }
+ catch (ArgumentException)
+ {
+ this.guardedOperations.HandleException(this,
+ new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Duplicate EditorOptionDefinition named {0}", name)));
+ }
+ }
+
+ internal EditorOptionDefinition GetOptionDefinition(string optionId)
+ {
+ lock (_instantiatedOptionDefinitions)
+ {
+ EditorOptionDefinition definition;
+ if (!_instantiatedOptionDefinitions.TryGetValue(optionId, out definition))
+ {
+ Lazy<EditorOptionDefinition, INameMetadata> import;
+ if (_namedOptionImports.TryGetValue(optionId, out import))
+ {
+ definition = this.guardedOperations.InstantiateExtension(import, import);
+
+ _namedOptionImports.Remove(optionId);
+ _instantiatedOptionDefinitions.Add(optionId, definition);
+ }
+ }
+
+ return definition;
+ }
+ }
+
+ internal EditorOptionDefinition GetOptionDefinitionOrThrow(string optionId)
+ {
+ var definition = this.GetOptionDefinition(optionId);
+ if (definition == null)
+ throw new ArgumentException(string.Format("No EditorOptionDefinition export found for the given option name: {0}", optionId), "optionId");
+
+ return definition;
+ }
+
+ internal IEnumerable<EditorOptionDefinition> GetSupportedOptions(IPropertyOwner scope)
+ {
+ //Unfortunately, to make this work, we need to instantiate everything. Do it immediately so that
+ //if someone does something like nesting calls to SupportedOptions we will have something stable.
+ lock (_instantiatedOptionDefinitions)
+ {
+ foreach (var import in _namedOptionImports)
+ {
+ var definition = this.guardedOperations.InstantiateExtension(import.Value, import.Value);
+ this.SafeAdd(_instantiatedOptionDefinitions, import.Key, definition); //Use the name from the metadata, not the name from the definition.
+ }
+
+ _namedOptionImports.Clear();
+ }
+
+ //At this point, _instantiatedOptionDefinitions should never change so we don't need to lock/copy.
+ foreach (var definition in _instantiatedOptionDefinitions.Values)
+ {
+ if ((scope == null) || definition.IsApplicableToScope(scope))
+ yield return definition;
+ }
+ }
+
+ internal IEnumerable<EditorOptionDefinition> GetInstantiatedOptions(IPropertyOwner scope)
+ {
+ List<EditorOptionDefinition> definitions;
+ lock(_instantiatedOptionDefinitions)
+ {
+ definitions = _instantiatedOptionDefinitions.Values.ToList();
+ }
+
+ foreach (var definition in definitions)
+ {
+ if ((scope == null) || definition.IsApplicableToScope(scope))
+ yield return definition;
+ }
+ }
+ }
+
+ public interface INameMetadata
+ {
+ [DefaultValue(null)]
+ string Name { get; }
+ }
+}
diff --git a/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs b/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs
new file mode 100644
index 0000000..55c0c89
--- /dev/null
+++ b/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs
@@ -0,0 +1,139 @@
+//
+// 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.
+//
+using System;
+using System.ComponentModel.Composition;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Internal options for text storage tuning.
+ /// </summary>
+ public static class TextModelEditorOptions
+ {
+ /// <summary>
+ /// The default option that determines the file size above which compressed storage will be used.
+ /// </summary>
+ public static readonly EditorOptionKey<int> CompressedStorageFileSizeThresholdOptionId = new EditorOptionKey<int>(CompressedStorageFileSizeThresholdOptionName);
+ public const string CompressedStorageFileSizeThresholdOptionName = "CompressedStorageFileSizeThreshold";
+
+ /// <summary>
+ /// The default option that determines the page size when compressed storage is in use.
+ /// </summary>
+ public static readonly EditorOptionKey<int> CompressedStoragePageSizeOptionId = new EditorOptionKey<int>(CompressedStoragePageSizeOptionName);
+ public const string CompressedStoragePageSizeOptionName = "CompressedStoragePageSize";
+
+ /// <summary>
+ /// The default option that specifies how many pages will be retained in memory when compressed storage is in use.
+ /// </summary>
+ public static readonly EditorOptionKey<int> CompressedStorageMaxLoadedPagesOptionId = new EditorOptionKey<int>(CompressedStorageMaxLoadedPagesOptionName);
+ public const string CompressedStorageMaxLoadedPagesOptionName = "CompressedStorageMaxLoadedPages";
+
+ /// <summary>
+ /// The default option that determines whether weak references to discarded pages will be retained.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> CompressedStorageRetainWeakReferencesOptionId = new EditorOptionKey<bool>(CompressedStorageRetainWeakReferencesOptionName);
+ public const string CompressedStorageRetainWeakReferencesOptionName = "CompressedStorageRetainWeakReferences";
+
+ /// <summary>
+ /// The default option that determines the size above which differencing of text changes will not be attempted even when requested.
+ /// </summary>
+ public static readonly EditorOptionKey<int> DiffSizeThresholdOptionId = new EditorOptionKey<int>(DiffSizeThresholdOptionName);
+ public const string DiffSizeThresholdOptionName = "DiffSizeThreshold";
+ }
+
+ /// <summary>
+ /// The option definition that determines the file size above which compressed storage will be used.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionName)]
+ public sealed class CompressedStorageFileSizeThreshold : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (5 megabytes)
+ /// </summary>
+ public override int Default { get { return 1024 * 1024 * 5; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines the page size when compressed storage is in use.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(TextModelEditorOptions.CompressedStoragePageSizeOptionName)]
+ public sealed class CompressedStoragePageSize : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (1 megabyte)
+ /// </summary>
+ public override int Default { get { return 1024 * 1024; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStoragePageSizeOptionId; } }
+ }
+
+ /// <summary>
+ /// The default option that specifies how many pages will be retained in memory when compressed storage is in use.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionName)]
+ public sealed class CompressedStorageMaxLoadedPages : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (3)
+ /// </summary>
+ public override int Default { get { return 3; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines whether weak references to discarded pages will be retained.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionName)]
+ public sealed class CompressedStorageRetainWeakReferences : EditorOptionDefinition<bool>
+ {
+ /// <summary>
+ /// Gets the default value (true)
+ /// </summary>
+ public override bool Default { get { return true; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionId; } }
+ }
+
+ /// <summary>
+ /// The default option that determines the size above which differencing of text changes will not be attempted even when requested.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(TextModelEditorOptions.DiffSizeThresholdOptionName)]
+ public sealed class DiffSizeThreshold : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (10 MB)
+ /// </summary>
+ public override int Default { get { return 25 * 1024 * 1024; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.DiffSizeThresholdOptionId; } }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs b/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs
new file mode 100644
index 0000000..65b814d
--- /dev/null
+++ b/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs
@@ -0,0 +1,29 @@
+//
+// 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.
+//
+using System.ComponentModel.Composition;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Editor.Implementation
+{
+ [Export(typeof(ITextModelOptionsSetter))]
+ public class TextModelOptionsSetter : ITextModelOptionsSetter
+ {
+ /// <summary>
+ /// Propagate options from editor options to the text model (editor options are not visible at the text model level).
+ /// </summary>
+ /// <param name="options"></param>
+ public void SetTextModelOptions(IEditorOptions options)
+ {
+ TextModelOptions.CompressedStorageFileSizeThreshold = options.GetOptionValue(TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionId);
+ TextModelOptions.CompressedStoragePageSize = options.GetOptionValue(TextModelEditorOptions.CompressedStoragePageSizeOptionId);
+ TextModelOptions.CompressedStorageMaxLoadedPages = options.GetOptionValue(TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionId);
+ TextModelOptions.CompressedStorageRetainWeakReferences = options.GetOptionValue(TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionId);
+ TextModelOptions.DiffSizeThreshold = options.GetOptionValue(TextModelEditorOptions.DiffSizeThresholdOptionId);
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs b/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs
new file mode 100644
index 0000000..6c760f6
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs
@@ -0,0 +1,30 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ internal sealed class BufferPrimitives : IBufferPrimitives
+ {
+ private TextBuffer _textBuffer;
+
+ public BufferPrimitives(ITextBuffer textBuffer, IBufferPrimitivesFactoryService bufferPrimitivesFactory)
+ {
+ _textBuffer = bufferPrimitivesFactory.CreateTextBuffer(textBuffer);
+ }
+ #region IBufferPrimitives Members
+
+ public TextBuffer Buffer
+ {
+ get { return _textBuffer; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs
new file mode 100644
index 0000000..19923d5
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs
@@ -0,0 +1,134 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ internal sealed class DefaultBufferPrimitive : TextBuffer
+ {
+ #region Private members
+ private ITextBuffer _textBuffer;
+ private IBufferPrimitivesFactoryService _bufferPrimitivesFactory;
+ #endregion
+
+ public DefaultBufferPrimitive(ITextBuffer textBuffer, IBufferPrimitivesFactoryService bufferPrimitivesFactory)
+ {
+ _textBuffer = textBuffer;
+ _bufferPrimitivesFactory = bufferPrimitivesFactory;
+ }
+
+ public override TextPoint GetTextPoint(int position)
+ {
+ if ((position < 0) || (position > _textBuffer.CurrentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ return _bufferPrimitivesFactory.CreateTextPoint(this, position);
+ }
+
+ public override TextPoint GetTextPoint(int line, int column)
+ {
+ if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount))
+ {
+ throw new ArgumentOutOfRangeException("line");
+ }
+
+ ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line);
+
+ if ((column < 0) || (column > snapshotLine.Length))
+ {
+ throw new ArgumentOutOfRangeException("column");
+ }
+ return _bufferPrimitivesFactory.CreateTextPoint(this, snapshotLine.Start + column);
+ }
+
+ public override TextRange GetLine(int line)
+ {
+ if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount))
+ {
+ throw new ArgumentOutOfRangeException("line");
+ }
+
+ ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line);
+
+ return GetTextRange(snapshotLine.Extent.Start, snapshotLine.Extent.End);
+ }
+
+ public override TextRange GetTextRange(TextPoint startPoint, TextPoint endPoint)
+ {
+ if (startPoint == null)
+ {
+ throw new ArgumentNullException("startPoint");
+ }
+ if (endPoint == null)
+ {
+ throw new ArgumentNullException("endPoint");
+ }
+
+ if (!object.ReferenceEquals(startPoint.TextBuffer, this))
+ {
+ throw new ArgumentException(Strings.TextPointFromWrongBuffer);
+ }
+
+ if (!object.ReferenceEquals(endPoint.TextBuffer, this))
+ {
+ throw new ArgumentException(Strings.TextPointFromWrongBuffer);
+ }
+
+ return _bufferPrimitivesFactory.CreateTextRange(this, startPoint, endPoint);
+ }
+
+ public override TextRange GetTextRange(int startPosition, int endPosition)
+ {
+ if ((startPosition < 0) || (startPosition > _textBuffer.CurrentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("startPosition");
+ }
+
+ if ((endPosition < 0) || (endPosition > _textBuffer.CurrentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("endPosition");
+ }
+
+ TextPoint startPoint = GetTextPoint(startPosition);
+ TextPoint endPoint = GetTextPoint(endPosition);
+
+ return _bufferPrimitivesFactory.CreateTextRange(this, startPoint, endPoint);
+ }
+
+ public override ITextBuffer AdvancedTextBuffer
+ {
+ get { return _textBuffer; }
+ }
+
+ public override TextPoint GetStartPoint()
+ {
+ return GetTextPoint(0);
+ }
+
+ public override TextPoint GetEndPoint()
+ {
+ return GetTextPoint(_textBuffer.CurrentSnapshot.Length);
+ }
+
+ public override IEnumerable<TextRange> Lines
+ {
+ get
+ {
+ foreach (ITextSnapshotLine line in _textBuffer.CurrentSnapshot.Lines)
+ {
+ yield return GetTextRange(line.Start, line.End);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs
new file mode 100644
index 0000000..31af8de
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs
@@ -0,0 +1,61 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ [Export(typeof(IBufferPrimitivesFactoryService))]
+ internal sealed class DefaultBufferPrimitivesFactoryService : IBufferPrimitivesFactoryService
+ {
+ [Import]
+ internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; }
+
+ [Import]
+ internal ITextSearchService TextSearchService { get; set; }
+
+ [Import]
+ internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelectorService { get; set; }
+
+ #region IBufferPrimitivesFactoryService Members
+
+ public TextBuffer CreateTextBuffer(ITextBuffer textBuffer)
+ {
+ TextBuffer textBufferPrimitive = null;
+ if (!textBuffer.Properties.TryGetProperty<TextBuffer>(EditorPrimitiveIds.BufferPrimitiveId, out textBufferPrimitive))
+ {
+ textBufferPrimitive = new DefaultBufferPrimitive(textBuffer, this);
+
+ textBuffer.Properties.AddProperty(EditorPrimitiveIds.BufferPrimitiveId, textBufferPrimitive);
+ }
+
+ return textBufferPrimitive;
+ }
+
+ public TextPoint CreateTextPoint(TextBuffer textBuffer, int position)
+ {
+ return new DefaultTextPointPrimitive(
+ textBuffer,
+ position,
+ TextSearchService,
+ EditorOptionsFactoryService.GetOptions(textBuffer.AdvancedTextBuffer),
+ TextStructureNavigatorSelectorService.GetTextStructureNavigator(textBuffer.AdvancedTextBuffer),
+ this);
+ }
+
+ public TextRange CreateTextRange(TextBuffer textBuffer, TextPoint startPoint, TextPoint endPoint)
+ {
+ return new DefaultTextRangePrimitive(startPoint, endPoint, EditorOptionsFactoryService);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs
new file mode 100644
index 0000000..145ebb3
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs
@@ -0,0 +1,978 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.Collections.ObjectModel;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+ internal sealed class DefaultCaretPrimitive : Caret
+ {
+ #region Private members
+ private TextView _textView;
+ private IEditorOptions _editorOptions;
+ #endregion
+
+ internal DefaultCaretPrimitive(TextView textView, IEditorOptions editorOptions)
+ {
+ _textView = textView;
+ _editorOptions = editorOptions;
+ }
+
+ public override void MoveToNextCharacter(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ TextPoint endPoint = TextView.Selection.GetEndPoint();
+ bool selectionIsEmpty = TextView.Selection.IsEmpty;
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ // If the selection is not empty, VS clears the selection and sets
+ // the caret at the end of the selection (regardless of it were reversed or not)
+ if (!extendSelection && !selectionIsEmpty)
+ {
+ MoveTo(endPoint.CurrentPosition);
+ }
+ else
+ {
+ AdvancedCaret.MoveToNextCaretPosition();
+ }
+
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToPreviousCharacter(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ TextPoint startPoint = TextView.Selection.GetStartPoint();
+ bool selectionIsEmpty = TextView.Selection.IsEmpty;
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ if (!extendSelection && !selectionIsEmpty)
+ {
+ MoveTo(startPoint.CurrentPosition);
+
+ }
+ else
+ {
+ AdvancedCaret.MoveToPreviousCaretPosition();
+ }
+
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToBeginningOfPreviousLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.MoveToBeginningOfPreviousLine();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToBeginningOfNextLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.MoveToBeginningOfNextLine();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToBeginningOfPreviousViewLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.MoveToBeginningOfPreviousViewLine();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToBeginningOfNextViewLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.MoveToBeginningOfNextViewLine();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToPreviousLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+
+ ITextViewLine caretLine;
+ if (this.TextView.Selection.IsEmpty || extendSelection)
+ {
+ caretLine = this.TextView.AdvancedTextView.Caret.ContainingTextViewLine;
+ }
+ else
+ {
+ caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(this.TextView.AdvancedTextView.Selection.Start.Position);
+ }
+
+ if (caretLine.Start != 0)
+ {
+ caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(caretLine.Start - 1);
+ }
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.AdvancedCaret.MoveTo(caretLine);
+ this.AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToNextLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+
+ ITextViewLine caretLine;
+ if (this.TextView.Selection.IsEmpty || extendSelection)
+ {
+ caretLine = this.TextView.AdvancedTextView.Caret.ContainingTextViewLine;
+ }
+ else
+ {
+ SnapshotPoint end = this.TextView.AdvancedTextView.Selection.End.Position;
+ caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(end);
+
+ if ((!caretLine.IsFirstTextViewLineForSnapshotLine) && (end.Position == caretLine.Start.Position))
+ {
+ //The end of the selection is at the seam between two word-wrapped lines. In this case, we want
+ //the line before (since the selection is drawn to the end of that line rather than to the beginning
+ //of the other).
+ caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(end - 1);
+ }
+ }
+
+ if ((!caretLine.IsLastTextViewLineForSnapshotLine) || (caretLine.LineBreakLength != 0)) //If we are not on the last line of the file
+ {
+ caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(caretLine.EndIncludingLineBreak);
+ }
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.AdvancedCaret.MoveTo(caretLine);
+ this.AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveTo(int position, bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ SnapshotPoint bufferPosition = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, position);
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ AdvancedCaret.MoveTo(bufferPosition);
+ AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MovePageUp()
+ {
+ PageUpDown(ScrollDirection.Up);
+ }
+
+ public override void MovePageDown()
+ {
+ PageUpDown(ScrollDirection.Down);
+ }
+
+ public override void MovePageUp(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ MovePageUp();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MovePageDown(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ MovePageDown();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToEndOfLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+
+ DisplayTextPoint caret = this.CaretPoint;
+ if ((!this.TextView.Selection.IsEmpty) && !extendSelection)
+ {
+ caret = this.TextView.GetTextPoint(this.TextView.Selection.AdvancedSelection.End.Position);
+ }
+ caret.MoveToEndOfLine();
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint, PositionAffinity.Successor);
+
+ this.AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToStartOfLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+
+ DisplayTextPoint caret = this.CaretPoint;
+ if ((!this.TextView.Selection.IsEmpty) && !extendSelection)
+ {
+ caret = this.TextView.GetTextPoint(this.TextView.Selection.AdvancedSelection.Start.Position);
+ }
+ caret.MoveToStartOfLine();
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint, PositionAffinity.Successor);
+
+ this.AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToEndOfViewLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ SnapshotPoint selectionEnd = this.TextView.Selection.AdvancedSelection.End.Position;
+ bool selectionIsEmpty = TextView.Selection.IsEmpty;
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ if (!selectionIsEmpty && !extendSelection)
+ {
+ var selectionEndLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(selectionEnd);
+ AdvancedCaret.MoveTo(selectionEndLine.End, PositionAffinity.Predecessor);
+ }
+ else
+ {
+ ITextViewLine line = this.AdvancedCaret.ContainingTextViewLine;
+ AdvancedCaret.MoveTo(line.End, line.IsLastTextViewLineForSnapshotLine ? PositionAffinity.Successor : PositionAffinity.Predecessor);
+ }
+
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToStartOfViewLine(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ SnapshotPoint selectionStart = this.TextView.Selection.AdvancedSelection.Start.Position;
+ bool selectionIsEmpty = TextView.Selection.IsEmpty;
+ bool selectionIsReversed = TextView.Selection.AdvancedSelection.IsReversed;
+
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ //We can't use the default behavior of the DisplayTextPoint.MoveToStartOfViewLine since the ITextViewLine
+ //the caret is on may not be the same as the ITextViewLine the DisplayTextPoint created from the caret thinks
+ //it is on (c.f. word wrap & affinity). Simulate the behavior of the DisplayTextPoint, adjusting for the
+ //ITextViewLine and selection.
+ ITextViewLine line;
+ if (!selectionIsEmpty && !selectionIsReversed && !extendSelection)
+ {
+ line = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(selectionStart);
+ }
+ else
+ {
+ line = this.AdvancedCaret.ContainingTextViewLine;
+ }
+
+ SnapshotPoint startOfLine = line.Start;
+
+ this.AdvancedCaret.MoveTo(startOfLine, PositionAffinity.Successor);
+ this.AdvancedCaret.EnsureVisible();
+ this.UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToLine(int lineNumber, bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ MoveToLine(lineNumber);
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToLine(int lineNumber, int offset, bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ ITextSnapshotLine line = TextView.AdvancedTextView.TextSnapshot.GetLineFromLineNumber(lineNumber);
+ AdvancedCaret.MoveTo(new VirtualSnapshotPoint(line, offset));
+
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToStartOfDocument(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ AdvancedCaret.MoveTo(new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, 0));
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToEndOfDocument(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ AdvancedCaret.MoveTo(new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot,
+ TextView.AdvancedTextView.TextSnapshot.Length));
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToNextWord(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ DisplayTextPoint caretLocation = CaretPoint;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ caretLocation.MoveToNextWord();
+ AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint);
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void MoveToPreviousWord(bool extendSelection)
+ {
+ VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition;
+ DisplayTextPoint caretLocation = CaretPoint;
+ if (!extendSelection)
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+
+ caretLocation.MoveToPreviousWord();
+ AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint);
+ AdvancedCaret.EnsureVisible();
+ UpdateSelection(extendSelection, oldCaretPoint);
+ }
+
+ public override void EnsureVisible()
+ {
+ AdvancedCaret.EnsureVisible();
+ }
+
+ public override ITextCaret AdvancedCaret
+ {
+ get { return _textView.AdvancedTextView.Caret; }
+ }
+
+ public override TextView TextView
+ {
+ get { return _textView; }
+ }
+
+ public override ITextViewLine AdvancedTextViewLine
+ {
+ get
+ {
+ return this.AdvancedCaret.ContainingTextViewLine;
+ }
+ }
+
+ public override int DisplayColumn
+ {
+ get
+ {
+ ITextViewLine textViewLine = AdvancedTextViewLine;
+
+ if ((textViewLine != null) && (textViewLine.IsValid))
+ {
+ return PrimitivesUtilities.GetColumnOfPoint(
+ TextView.AdvancedTextView.TextSnapshot,
+ AdvancedTextPoint,
+ textViewLine.Start,
+ _editorOptions.GetTabSize(),
+ (p) => (textViewLine.GetTextElementSpan(p).End));
+ }
+ else
+ {
+ return CaretPoint.DisplayColumn;
+ }
+ }
+ }
+
+ public override bool IsVisible
+ {
+ get { return CaretPoint.IsVisible; }
+ }
+
+ public override DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint)
+ {
+ return CaretPoint.GetDisplayTextRange(otherPoint);
+ }
+
+ public override DisplayTextRange GetDisplayTextRange(int otherPosition)
+ {
+ return CaretPoint.GetDisplayTextRange(otherPosition);
+ }
+
+ protected override DisplayTextPoint CloneDisplayTextPointInternal()
+ {
+ return CaretPoint.Clone();
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _textView.TextBuffer; }
+ }
+
+ public override int CurrentPosition
+ {
+ get { return CaretPoint.CurrentPosition; }
+ }
+
+ public override int Column
+ {
+ get { return CaretPoint.Column; }
+ }
+
+ public override bool DeleteNext()
+ {
+ if (TextView.Selection.IsEmpty)
+ {
+ if (!CaretPoint.DeleteNext())
+ return false;
+ }
+ else
+ {
+ if (!TextView.Selection.Delete())
+ return false;
+ }
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override bool DeletePrevious()
+ {
+ if (TextView.Selection.IsEmpty)
+ {
+ if (AdvancedCaret.InVirtualSpace)
+ {
+ AdvancedCaret.MoveToPreviousCaretPosition();
+ }
+ else
+ {
+ if (!CaretPoint.DeletePrevious())
+ return false;
+ if (AdvancedCaret.InVirtualSpace)
+ AdvancedCaret.MoveTo(AdvancedCaret.Position.BufferPosition);
+ }
+ }
+ else
+ {
+ if (!TextView.Selection.Delete())
+ return false;
+ }
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override TextRange GetCurrentWord()
+ {
+ return CaretPoint.GetCurrentWord();
+ }
+
+ public override TextRange GetNextWord()
+ {
+ return CaretPoint.GetNextWord();
+ }
+
+ public override TextRange GetPreviousWord()
+ {
+ return CaretPoint.GetPreviousWord();
+ }
+
+ public override TextRange GetTextRange(TextPoint otherPoint)
+ {
+ return CaretPoint.GetTextRange(otherPoint);
+ }
+
+ public override TextRange GetTextRange(int otherPosition)
+ {
+ return CaretPoint.GetTextRange(otherPosition);
+ }
+
+ public override bool InsertNewLine()
+ {
+ if (!TextView.Selection.IsEmpty)
+ {
+ if (!TextView.Selection.Delete())
+ return false;
+ }
+
+ if (!CaretPoint.InsertNewLine())
+ return false;
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override bool InsertIndent()
+ {
+ if (!CaretPoint.InsertIndent())
+ return false;
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override bool InsertText(string text)
+ {
+ if (!TextView.Selection.IsEmpty)
+ {
+ if (!TextView.Selection.Delete())
+ return false;
+ }
+
+ if (!CaretPoint.InsertText(text))
+ return false;
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override int LineNumber
+ {
+ get { return CaretPoint.LineNumber; }
+ }
+
+ public override int StartOfLine
+ {
+ get { return CaretPoint.StartOfLine; }
+ }
+
+ public override int EndOfLine
+ {
+ get { return CaretPoint.EndOfLine; }
+ }
+
+ public override int StartOfViewLine
+ {
+ get { return this.AdvancedTextViewLine.Start; }
+ }
+
+ public override int EndOfViewLine
+ {
+ get { return this.AdvancedTextViewLine.End; }
+ }
+
+ public override bool RemovePreviousIndent()
+ {
+ if (!CaretPoint.RemovePreviousIndent())
+ return false;
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override bool TransposeCharacter()
+ {
+ if (!CaretPoint.TransposeCharacter())
+ return false;
+
+ TextView.Selection.Clear();
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override bool TransposeLine()
+ {
+ if (TextView.Selection.IsEmpty)
+ {
+ TextPoint caretPoint = CaretPoint;
+ double oldLeft = AdvancedCaret.Left;
+
+ if (!caretPoint.TransposeLine())
+ return false;
+
+ AdvancedCaret.MoveTo(caretPoint.AdvancedTextPoint, PositionAffinity.Successor, false);
+ AdvancedCaret.EnsureVisible();
+
+ ITextViewLine newLine = AdvancedTextViewLine;
+ AdvancedCaret.MoveTo(newLine, oldLeft);
+ AdvancedCaret.EnsureVisible();
+ }
+ return true;
+ }
+
+ public override bool TransposeLine(int lineNumber)
+ {
+ if (!TextView.Selection.IsEmpty)
+ return true;
+
+ if (!CaretPoint.TransposeLine(lineNumber))
+ return false;
+
+ AdvancedCaret.EnsureVisible();
+
+ return true;
+ }
+
+ public override SnapshotPoint AdvancedTextPoint
+ {
+ get { return CaretPoint.AdvancedTextPoint; }
+ }
+
+ public override string GetNextCharacter()
+ {
+ return CaretPoint.GetNextCharacter();
+ }
+
+ public override string GetPreviousCharacter()
+ {
+ return CaretPoint.GetPreviousCharacter();
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ return CaretPoint.Find(pattern, findOptions, endPoint);
+ }
+
+ public override TextRange Find(string pattern, TextPoint endPoint)
+ {
+ return CaretPoint.Find(pattern, endPoint);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ return CaretPoint.Find(pattern, findOptions);
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ return CaretPoint.Find(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint)
+ {
+ return CaretPoint.FindAll(pattern, endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ return CaretPoint.FindAll(pattern, findOptions, endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return CaretPoint.FindAll(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ return CaretPoint.FindAll(pattern, findOptions);
+ }
+
+ public override void MoveTo(int position)
+ {
+ MoveTo(position, false);
+ }
+
+ public override void MoveToNextCharacter()
+ {
+ MoveToNextCharacter(false);
+ }
+
+ public override void MoveToPreviousCharacter()
+ {
+ MoveToPreviousCharacter(false);
+ }
+
+ public override void MoveToLine(int lineNumber)
+ {
+ DisplayTextPoint caretLocation = CaretPoint;
+ TextView.Selection.Clear();
+ caretLocation.MoveToLine(lineNumber);
+ AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint);
+ AdvancedCaret.EnsureVisible();
+ }
+
+ public override void MoveToEndOfLine()
+ {
+ MoveToEndOfLine(false);
+ }
+
+ public override void MoveToStartOfLine()
+ {
+ MoveToStartOfLine(false);
+ }
+
+ public override void MoveToEndOfViewLine()
+ {
+ MoveToEndOfViewLine(false);
+ }
+
+ public override void MoveToStartOfViewLine()
+ {
+ MoveToStartOfViewLine(false);
+ }
+
+ public override void MoveToEndOfDocument()
+ {
+ MoveToEndOfDocument(false);
+ }
+
+ public override void MoveToStartOfDocument()
+ {
+ MoveToStartOfDocument(false);
+ }
+
+ public override void MoveToBeginningOfNextLine()
+ {
+ DisplayTextPoint caret = this.CaretPoint;
+ caret.MoveToBeginningOfNextLine();
+ this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint);
+ this.AdvancedCaret.EnsureVisible();
+ }
+
+ public override void MoveToBeginningOfPreviousLine()
+ {
+ DisplayTextPoint caret = this.CaretPoint;
+ caret.MoveToBeginningOfPreviousLine();
+ this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint);
+ this.AdvancedCaret.EnsureVisible();
+ }
+
+ public override void MoveToBeginningOfNextViewLine()
+ {
+ this.AdvancedCaret.MoveTo(this.AdvancedTextViewLine.EndIncludingLineBreak);
+ this.AdvancedCaret.EnsureVisible();
+ }
+
+ public override void MoveToBeginningOfPreviousViewLine()
+ {
+ ITextViewLine line = this.AdvancedTextViewLine;
+ if (line.Start > 0)
+ line = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(line.Start - 1);
+
+ this.AdvancedCaret.MoveTo(line.Start);
+ this.AdvancedCaret.EnsureVisible();
+ }
+
+ public override void MoveToNextWord()
+ {
+ MoveToNextWord(false);
+ }
+
+ public override void MoveToPreviousWord()
+ {
+ MoveToPreviousWord(false);
+ }
+
+ // Note: This overrides the equivalent DisplayTextPoint method in order to use the caret's
+ // ContainingTextViewLine, which can differ at the wrap points of word-wrapped lines.
+ public override DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine()
+ {
+ ITextViewLine viewLine = this.AdvancedTextViewLine;
+ ITextSnapshot snapshot = viewLine.Extent.Snapshot;
+
+ int firstNonWhitespaceCharacter = viewLine.Start;
+ while ((firstNonWhitespaceCharacter < viewLine.End) &&
+ char.IsWhiteSpace(snapshot[firstNonWhitespaceCharacter]))
+ {
+ firstNonWhitespaceCharacter++;
+ }
+
+ return TextView.GetTextPoint(firstNonWhitespaceCharacter);
+ }
+
+ public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine()
+ {
+ return CaretPoint.GetFirstNonWhiteSpaceCharacterOnLine();
+ }
+
+ #region Private methods
+ private DisplayTextPoint CaretPoint
+ {
+ get
+ {
+ return TextView.GetTextPoint(_textView.AdvancedTextView.Caret.Position.BufferPosition);
+ }
+ }
+
+ /// <summary>
+ /// Update the selection based on the movement of the caret.
+ /// </summary>
+ private void UpdateSelection(bool extendSelection, VirtualSnapshotPoint oldPosition)
+ {
+ if (extendSelection)
+ {
+ if (TextView.Selection.IsEmpty)
+ {
+ //The provided old position might be stale
+ this.TextView.AdvancedTextView.Selection.Select(oldPosition.TranslateTo(this.TextView.AdvancedTextView.TextSnapshot),
+ this.AdvancedCaret.Position.VirtualBufferPosition);
+ }
+ else
+ {
+ this.TextView.AdvancedTextView.Selection.Select(this.TextView.AdvancedTextView.Selection.AnchorPoint,
+ this.AdvancedCaret.Position.VirtualBufferPosition);
+ }
+ }
+ else
+ {
+ TextView.AdvancedTextView.Selection.Clear();
+ }
+ }
+
+ private void PageUpDown(ScrollDirection direction)
+ {
+ if (direction == ScrollDirection.Up)
+ {
+ //If we scrolled a full page, then the bottom of the first fully visible line will be below
+ //the bottom of the view.
+ ITextViewLine firstVisibleLine = TextView.AdvancedTextView.TextViewLines.FirstVisibleLine;
+ SnapshotPoint oldFullyVisibleStart = (firstVisibleLine.VisibilityState == VisibilityState.FullyVisible)
+ ? firstVisibleLine.Start
+ : firstVisibleLine.EndIncludingLineBreak; //Start of next line.
+
+ if (TextView.AdvancedTextView.ViewScroller.ScrollViewportVerticallyByPage(direction))
+ {
+ ITextViewLine newFirstLine = TextView.AdvancedTextView.TextViewLines.GetTextViewLineContainingBufferPosition(oldFullyVisibleStart);
+
+ //The old fully visible line should -- if we scrolled as much as we could -- be partially
+ //obscured. The shortfall between a full page and what we actually scrolled is the distance
+ //between the bottom of that line and the bottom of the screen.
+ if (TextView.AdvancedTextView.ViewportBottom > newFirstLine.Bottom)
+ {
+ AdvancedCaret.MoveTo(TextView.AdvancedTextView.TextViewLines.FirstVisibleLine);
+ }
+ else
+ {
+ AdvancedCaret.MoveToPreferredCoordinates();
+ }
+ }
+ }
+ else
+ {
+ //If we scroll a full page, the bottom of the last fully visible line will be
+ //positioned at 0.0.
+ ITextViewLine lastVisibleLine = TextView.AdvancedTextView.TextViewLines.LastVisibleLine;
+
+ // If the last line in the buffer is fully visible , then just move the caret to
+ // the last visible line
+ if ((lastVisibleLine.VisibilityState == VisibilityState.FullyVisible) &&
+ (lastVisibleLine.End == lastVisibleLine.Snapshot.Length))
+ {
+ AdvancedCaret.MoveTo(lastVisibleLine);
+
+ // No need to ensure the caret is visible since the line it just moved to is fully visible.
+ return;
+ }
+
+ SnapshotPoint oldFullyVisibleStart = ((lastVisibleLine.VisibilityState == VisibilityState.FullyVisible) || (lastVisibleLine.Start == 0))
+ ? lastVisibleLine.Start
+ : (lastVisibleLine.Start - 1); //Actually just a point on the previous line.
+
+ if (TextView.AdvancedTextView.ViewScroller.ScrollViewportVerticallyByPage(direction))
+ {
+ ITextViewLine newLastLine = TextView.AdvancedTextView.TextViewLines.GetTextViewLineContainingBufferPosition(oldFullyVisibleStart);
+ if (newLastLine.Bottom > TextView.AdvancedTextView.ViewportTop)
+ {
+ AdvancedCaret.MoveTo(TextView.AdvancedTextView.TextViewLines.LastVisibleLine);
+ }
+ else
+ {
+ AdvancedCaret.MoveToPreferredCoordinates();
+ }
+ }
+ }
+
+ EnsureVisible();
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs
new file mode 100644
index 0000000..0b64bc4
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs
@@ -0,0 +1,499 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.Collections.ObjectModel;
+ using System.Globalization;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+ using System.Diagnostics;
+
+ internal sealed class DefaultDisplayTextPointPrimitive : DisplayTextPoint
+ {
+ private TextView _textView;
+ private TextPoint _bufferPoint;
+ private IEditorOptions _editorOptions;
+
+ internal DefaultDisplayTextPointPrimitive(TextView textView, int position, IEditorOptions editorOptions)
+ {
+ _textView = textView;
+ _editorOptions = editorOptions;
+
+ _bufferPoint = _textView.TextBuffer.GetStartPoint();
+
+ // The position coming in will be from the view's snapshot which may not be the same snapshot as the buffer.
+ // Force the position to track to the buffer's snapshot to prevent the position from being valid in the view's
+ // snapshot but invalid in the buffers
+ ITrackingPoint trackingPoint = _textView.AdvancedTextView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Positive);
+
+ MoveTo(trackingPoint.GetPosition(_textView.TextBuffer.AdvancedTextBuffer.CurrentSnapshot));
+ }
+
+ public override TextView TextView
+ {
+ get { return _textView; }
+ }
+
+ public override ITextViewLine AdvancedTextViewLine
+ {
+ get
+ {
+ return _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(this.AdvancedTextPoint);
+ }
+ }
+
+ public override int DisplayColumn
+ {
+ get
+ {
+ ITextView textView = _textView.AdvancedTextView;
+ SnapshotPoint position = this.AdvancedTextViewLine.Start;
+
+ return PrimitivesUtilities.GetColumnOfPoint(
+ textView.TextSnapshot,
+ AdvancedTextPoint,
+ position,
+ _editorOptions.GetTabSize(),
+ (p) => (textView.GetTextElementSpan(p).End));
+ }
+ }
+
+ public override bool IsVisible
+ {
+ get
+ {
+ // if no lines are being drawn then this point can't be visible
+ return (this.AdvancedTextViewLine.VisibilityState == VisibilityState.FullyVisible);
+ }
+ }
+
+ public override DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint)
+ {
+ if (!object.ReferenceEquals(this.TextBuffer, otherPoint.TextBuffer))
+ {
+ throw new ArgumentException("The other point must have the same TextBuffer as this one", "otherPoint");
+ }
+ return TextView.GetTextRange(this, otherPoint);
+ }
+
+ public override DisplayTextRange GetDisplayTextRange(int otherPosition)
+ {
+ return TextView.GetTextRange(CurrentPosition, otherPosition);
+ }
+
+ protected override DisplayTextPoint CloneDisplayTextPointInternal()
+ {
+ return new DefaultDisplayTextPointPrimitive(_textView, CurrentPosition, _editorOptions);
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _textView.TextBuffer; }
+ }
+
+ public override int CurrentPosition
+ {
+ get { return _bufferPoint.CurrentPosition; }
+ }
+
+ public override int Column
+ {
+ get { return _bufferPoint.Column; }
+ }
+
+ public override bool DeleteNext()
+ {
+ if (_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(AdvancedTextPoint, PositionAffinity.Successor))
+ {
+ return PrimitivesUtilities.Delete(GetNextTextElementSpan());
+ }
+ else
+ {
+ return _bufferPoint.DeleteNext();
+ }
+ }
+
+ public override bool DeletePrevious()
+ {
+ SnapshotSpan previousElementSpan = GetPreviousTextElementSpan();
+
+ if ((previousElementSpan.Length > 0) &&
+ (_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(AdvancedTextPoint, PositionAffinity.Successor)) &&
+ (!_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(previousElementSpan.End - 1, PositionAffinity.Successor)))
+ {
+ // Since the previous character is not visible but the current one is, delete
+ // the entire previous text element span.
+ return PrimitivesUtilities.Delete(previousElementSpan);
+ }
+ else
+ {
+ // Delegate to the buffer point's DeletePrevious implementation to handle deleting single
+ // characters. A single character should be deleted if this point and the previous one
+ // are both visible or both not visible.
+ return _bufferPoint.DeletePrevious();
+ }
+ }
+
+ public override TextRange GetCurrentWord()
+ {
+ return _bufferPoint.GetCurrentWord();
+ }
+
+ public override TextRange GetNextWord()
+ {
+ return _bufferPoint.GetNextWord();
+ }
+
+ public override TextRange GetPreviousWord()
+ {
+ return _bufferPoint.GetPreviousWord();
+ }
+
+ public override TextRange GetTextRange(TextPoint otherPoint)
+ {
+ return _bufferPoint.GetTextRange(otherPoint);
+ }
+
+ public override TextRange GetTextRange(int otherPosition)
+ {
+ return _bufferPoint.GetTextRange(otherPosition);
+ }
+
+ public override bool InsertNewLine()
+ {
+ return _bufferPoint.InsertNewLine();
+ }
+
+ public override bool InsertIndent()
+ {
+ return _bufferPoint.InsertIndent();
+ }
+
+ public override bool InsertText(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ return _bufferPoint.InsertText(text);
+ }
+
+ public override int LineNumber
+ {
+ get { return _bufferPoint.LineNumber; }
+ }
+
+ public override int StartOfLine
+ {
+ get { return _bufferPoint.StartOfLine; }
+ }
+
+ public override int EndOfLine
+ {
+ get { return _bufferPoint.EndOfLine; }
+ }
+
+ public override int StartOfViewLine
+ {
+ get
+ {
+ return this.AdvancedTextViewLine.Start;
+ }
+ }
+
+ public override int EndOfViewLine
+ {
+ get
+ {
+ return this.AdvancedTextViewLine.End;
+ }
+ }
+
+ public override bool RemovePreviousIndent()
+ {
+ return _bufferPoint.RemovePreviousIndent();
+ }
+
+ public override bool TransposeCharacter()
+ {
+ SnapshotPoint insertionPoint = AdvancedTextPoint;
+
+ ITextSnapshotLine line = _textView.AdvancedTextView.TextSnapshot.GetLineFromPosition(insertionPoint);
+
+ string lineText = line.GetText();
+
+ if (StringInfo.ParseCombiningCharacters(lineText).Length < 2)
+ {
+ return true;
+ }
+
+ SnapshotSpan textElementLeftSpan, textElementRightSpan;
+
+ // We're at the start of a line
+ if (insertionPoint == line.Start)
+ {
+ textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint);
+ textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementLeftSpan.End);
+ }
+ // We're at the end of a line
+ else if (insertionPoint == line.End)
+ {
+ textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint - 1);
+ textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementRightSpan.Start - 1);
+ }
+ // We're at the middle of a line
+ else
+ {
+ textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint);
+ textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementRightSpan.Start - 1);
+ }
+
+ string transposedText = _textView.AdvancedTextView.TextSnapshot.GetText(textElementRightSpan)
+ + _textView.AdvancedTextView.TextSnapshot.GetText(textElementLeftSpan);
+
+ return PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, new Span(textElementLeftSpan.Start, transposedText.Length), transposedText);
+ }
+
+ public override bool TransposeLine()
+ {
+ return _bufferPoint.TransposeLine();
+ }
+
+ public override bool TransposeLine(int lineNumber)
+ {
+ return _bufferPoint.TransposeLine(lineNumber);
+ }
+
+ public override SnapshotPoint AdvancedTextPoint
+ {
+ // TODO!: Should this not translate (and throw instead)? Should it be positive or negative?
+ // Use positive tracking to behave like the caret would.
+ get
+ {
+ Debug.Assert(_bufferPoint.AdvancedTextPoint.Snapshot == TextView.AdvancedTextView.TextSnapshot,
+ "WARNING: We are tracking a SnapshotPoint in a display primitive, which could have bad consequences.");
+
+ return _bufferPoint.AdvancedTextPoint.TranslateTo(TextView.AdvancedTextView.TextSnapshot,
+ PointTrackingMode.Positive);
+ }
+ }
+
+ public override string GetNextCharacter()
+ {
+ if (CurrentPosition == _textView.AdvancedTextView.TextSnapshot.Length)
+ {
+ return string.Empty;
+ }
+ return _textView.AdvancedTextView.TextSnapshot.GetText(TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint));
+ }
+
+ public override string GetPreviousCharacter()
+ {
+ if (CurrentPosition == 0)
+ {
+ return string.Empty;
+ }
+ return _textView.AdvancedTextView.TextSnapshot.GetText(GetPreviousTextElementSpan());
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ return _bufferPoint.Find(pattern, findOptions, endPoint);
+ }
+
+ public override TextRange Find(string pattern, TextPoint endPoint)
+ {
+ return _bufferPoint.Find(pattern, endPoint);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ return _bufferPoint.Find(pattern, findOptions);
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ return _bufferPoint.Find(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint)
+ {
+ return _bufferPoint.FindAll(pattern, endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ return _bufferPoint.FindAll(pattern, findOptions, endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return _bufferPoint.FindAll(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ return _bufferPoint.FindAll(pattern, findOptions);
+ }
+
+ public override void MoveTo(int position)
+ {
+ SnapshotPoint point = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, position);
+
+ _bufferPoint.MoveTo(TextView.AdvancedTextView.GetTextElementSpan(point).Start);
+ }
+
+ public override void MoveToNextCharacter()
+ {
+ int currentPosition = this.CurrentPosition;
+
+ MoveTo(GetNextTextElementSpan().End);
+
+ // It's possible that the above code didn't actually move this point. Dev11 #109752 shows how
+ // this can happen, which is that GetNextTextElementSpan() above (which calls into WPF) may say
+ // that the current character doesn't combine with anything, but the _bufferPoint (which calls into
+ // framework methods that follow the unicode standard) will say that the character does combine. In that case
+ // the GetNextTextElementSpan().End isn't far enough away for this point to escape the combining character
+ // sequence, and the point is left where it is, at the start of a base character.
+ if (currentPosition == this.CurrentPosition &&
+ currentPosition != this.AdvancedTextPoint.Snapshot.Length)
+ {
+ _bufferPoint.MoveToNextCharacter();
+
+ Debug.Assert(currentPosition != this.CurrentPosition,
+ "DisplayTextPoint.MoveToNextCharacter was unable to successfully move forward. This may cause unexpected behavior or hangs.");
+ }
+ }
+
+ public override void MoveToPreviousCharacter()
+ {
+ MoveTo(GetPreviousTextElementSpan().Start);
+ }
+
+ public override void MoveToLine(int lineNumber)
+ {
+ _bufferPoint.MoveToLine(lineNumber);
+ }
+
+ public override void MoveToEndOfLine()
+ {
+ _bufferPoint.MoveToEndOfLine();
+ }
+
+ public override void MoveToStartOfLine()
+ {
+ _bufferPoint.MoveToStartOfLine();
+ }
+
+ public override void MoveToEndOfViewLine()
+ {
+ MoveTo(this.EndOfViewLine);
+ }
+
+ public override void MoveToStartOfViewLine()
+ {
+ MoveTo(this.StartOfViewLine);
+ }
+
+ public override void MoveToEndOfDocument()
+ {
+ _bufferPoint.MoveToEndOfDocument();
+ }
+
+ public override void MoveToStartOfDocument()
+ {
+ _bufferPoint.MoveToStartOfDocument();
+ }
+
+ public override void MoveToBeginningOfNextLine()
+ {
+ _bufferPoint.MoveToBeginningOfNextLine();
+ }
+
+ public override void MoveToBeginningOfPreviousLine()
+ {
+ _bufferPoint.MoveToBeginningOfPreviousLine();
+ }
+
+ public override void MoveToBeginningOfNextViewLine()
+ {
+ this.MoveTo(this.AdvancedTextViewLine.EndIncludingLineBreak);
+ }
+
+ public override void MoveToBeginningOfPreviousViewLine()
+ {
+ ITextViewLine line = this.AdvancedTextViewLine;
+ if (line.Start > 0)
+ line = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(line.Start - 1);
+
+ this.MoveTo(line.Start);
+ }
+
+ public override void MoveToNextWord()
+ {
+ _bufferPoint.MoveToNextWord();
+
+ // make sure the word structure navigator didn't return a position in the middle of a view element
+ SnapshotPoint bufferPoint = _bufferPoint.AdvancedTextPoint;
+ SnapshotSpan textElementSpan = _textView.AdvancedTextView.GetTextElementSpan(bufferPoint);
+ if (bufferPoint > textElementSpan.Start)
+ this.MoveTo(textElementSpan.End);
+ }
+
+ public override void MoveToPreviousWord()
+ {
+ _bufferPoint.MoveToPreviousWord();
+
+ // make sure the word structure navigator didn't return a position in the middle of a view element
+ this.MoveTo(_bufferPoint.AdvancedTextPoint);
+ }
+
+ public override DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine()
+ {
+ ITextViewLine viewLine = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(AdvancedTextPoint);
+ ITextSnapshot snapshot = viewLine.Extent.Snapshot;
+
+ int firstNonWhitespaceCharacter = viewLine.Start;
+ while ((firstNonWhitespaceCharacter < viewLine.End) &&
+ char.IsWhiteSpace(snapshot[firstNonWhitespaceCharacter]))
+ {
+ firstNonWhitespaceCharacter++;
+ }
+
+ return new DefaultDisplayTextPointPrimitive(_textView, firstNonWhitespaceCharacter, _editorOptions);
+ }
+
+ public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine()
+ {
+ return _bufferPoint.GetFirstNonWhiteSpaceCharacterOnLine();
+ }
+
+ #region Private helpers
+ private SnapshotSpan GetNextTextElementSpan()
+ {
+ return TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint);
+ }
+
+ private SnapshotSpan GetPreviousTextElementSpan()
+ {
+ if (CurrentPosition == 0)
+ {
+ return new SnapshotSpan(AdvancedTextPoint, 0);
+ }
+ return TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint - 1);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs
new file mode 100644
index 0000000..784892d
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs
@@ -0,0 +1,225 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ internal sealed class DefaultDisplayTextRangePrimitive : DisplayTextRange
+ {
+ private TextView _textView;
+ private TextRange _bufferRange;
+
+ internal DefaultDisplayTextRangePrimitive(TextView textView, TextRange bufferRange)
+ {
+ _textView = textView;
+ _bufferRange = bufferRange.Clone();
+
+ MoveTo(bufferRange);
+ }
+
+ public override TextView TextView
+ {
+ get { return _textView; }
+ }
+
+ public override DisplayTextPoint GetDisplayStartPoint()
+ {
+ return TextView.GetTextPoint(_bufferRange.GetStartPoint());
+ }
+
+ public override DisplayTextPoint GetDisplayEndPoint()
+ {
+ return TextView.GetTextPoint(_bufferRange.GetEndPoint());
+ }
+
+ public override VisibilityState Visibility
+ {
+ get
+ {
+ ITextViewLineCollection renderedTextLines = TextView.AdvancedTextView.TextViewLines;
+
+ SnapshotSpan range = new SnapshotSpan(GetStartPoint().AdvancedTextPoint,
+ GetEndPoint().AdvancedTextPoint);
+ SnapshotSpan? overlap = renderedTextLines.FormattedSpan.Overlap(range);
+
+ if (overlap.HasValue)
+ {
+ ITextViewLine startLine = renderedTextLines.GetTextViewLineContainingBufferPosition(overlap.Value.Start);
+ ITextViewLine endLine = renderedTextLines.GetTextViewLineContainingBufferPosition(overlap.Value.End);
+
+ VisibilityState startVisibility = startLine.VisibilityState;
+
+ if (startLine == endLine)
+ {
+ //Only a single line: visibility is whatever that line is.
+ return startVisibility;
+ }
+
+ VisibilityState endVisibility = endLine.VisibilityState;
+
+ if ((startVisibility == VisibilityState.FullyVisible) &&
+ (endVisibility == VisibilityState.FullyVisible))
+ {
+ //Both lines are fully visible and so is everything in between them.
+ return VisibilityState.FullyVisible;
+ }
+
+ if ((startVisibility != VisibilityState.Hidden) ||
+ (endVisibility != VisibilityState.Hidden) ||
+ (startLine.EndIncludingLineBreak != endLine.Start))
+ {
+ //Either one of the lines is not hidden (so the range is partially visible) or
+ //there are lines between the start that can't be hidden.
+ return VisibilityState.PartiallyVisible;
+ }
+ }
+
+ return VisibilityState.Hidden;
+ }
+ }
+
+ public override bool IsEmpty
+ {
+ get { return _bufferRange.IsEmpty; }
+ }
+
+ protected override DisplayTextRange CloneDisplayTextRangeInternal()
+ {
+ return new DefaultDisplayTextRangePrimitive(_textView, _bufferRange);
+ }
+
+ protected override IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal()
+ {
+ DisplayTextPoint displayTextPoint = GetDisplayStartPoint();
+ DisplayTextPoint endPoint = GetDisplayEndPoint();
+ while (displayTextPoint.CurrentPosition <= endPoint.CurrentPosition)
+ {
+ yield return displayTextPoint;
+
+ if (displayTextPoint.CurrentPosition == displayTextPoint.AdvancedTextPoint.Snapshot.Length)
+ {
+ break;
+ }
+ displayTextPoint = displayTextPoint.Clone();
+ displayTextPoint.MoveToNextCharacter();
+ }
+ }
+
+ public override TextPoint GetStartPoint()
+ {
+ return _bufferRange.GetStartPoint();
+ }
+
+ public override TextPoint GetEndPoint()
+ {
+ return _bufferRange.GetEndPoint();
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _textView.TextBuffer; }
+ }
+
+ public override SnapshotSpan AdvancedTextRange
+ {
+ get { return _bufferRange.AdvancedTextRange; }
+ }
+
+ public override bool MakeUppercase()
+ {
+ return _bufferRange.MakeUppercase();
+ }
+
+ public override bool MakeLowercase()
+ {
+ return _bufferRange.MakeLowercase();
+ }
+
+ public override bool Capitalize()
+ {
+ return _bufferRange.Capitalize();
+ }
+
+ public override bool ToggleCase()
+ {
+ return _bufferRange.ToggleCase();
+ }
+
+ public override bool Delete()
+ {
+ return _bufferRange.Delete();
+ }
+
+ public override bool Indent()
+ {
+ return _bufferRange.Indent();
+ }
+
+ public override bool Unindent()
+ {
+ return _bufferRange.Unindent();
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ return _bufferRange.Find(pattern);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ return _bufferRange.Find(pattern, findOptions);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return _bufferRange.FindAll(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ return _bufferRange.FindAll(pattern, findOptions);
+ }
+
+ public override bool ReplaceText(string newText)
+ {
+ return _bufferRange.ReplaceText(newText);
+ }
+
+ public override string GetText()
+ {
+ return _bufferRange.GetText();
+ }
+
+ public override void SetStart(TextPoint startPoint)
+ {
+ _bufferRange.SetStart(TextView.GetTextPoint(startPoint));
+ }
+
+ public override void SetEnd(TextPoint endPoint)
+ {
+ _bufferRange.SetEnd(TextView.GetTextPoint(endPoint));
+ }
+
+ public override void MoveTo(TextRange newRange)
+ {
+ SetStart(newRange.GetStartPoint());
+ SetEnd(newRange.GetEndPoint());
+ }
+
+ protected override IEnumerator<TextPoint> GetEnumeratorInternal()
+ {
+ return _bufferRange.GetEnumerator();
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs
new file mode 100644
index 0000000..35231be
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs
@@ -0,0 +1,496 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+ internal sealed class DefaultSelectionPrimitive : Selection
+ {
+ private TextView _textView;
+ private IEditorOptions _editorOptions;
+
+ public DefaultSelectionPrimitive(TextView textView, IEditorOptions editorOptions)
+ {
+ _textView = textView;
+ _editorOptions = editorOptions;
+ }
+
+ private ITextSelection TextSelection
+ {
+ get { return _textView.AdvancedTextView.Selection; }
+ }
+
+ private ITextCaret Caret
+ {
+ get { return TextView.AdvancedTextView.Caret; }
+ }
+
+ private DisplayTextRange TextRange
+ {
+ get
+ {
+ // TODO: What do we do in box mode?
+ return TextView.GetTextRange(TextSelection.Start.Position, TextSelection.End.Position);
+ }
+ }
+
+ public override void SelectRange(TextRange textRange)
+ {
+ this.SelectRange(textRange.GetStartPoint().CurrentPosition, textRange.GetEndPoint().CurrentPosition);
+ }
+
+ public override void SelectRange(TextPoint selectionStart, TextPoint selectionEnd)
+ {
+ this.SelectRange(selectionStart.CurrentPosition, selectionEnd.CurrentPosition);
+ }
+
+ public override void SelectAll()
+ {
+ // For select all, the selection always goes back to stream mode.
+ this.AdvancedSelection.Mode = TextSelectionMode.Stream;
+ this.SelectRange(0, TextView.AdvancedTextView.TextSnapshot.Length);
+ }
+
+ public override void ExtendSelection(TextPoint newEnd)
+ {
+ int selectionStart, selectionEnd = newEnd.CurrentPosition;
+
+ // Now, figure out where the selection actually started
+ if (IsEmpty)
+ selectionStart = TextSelection.Start.Position;
+ else
+ {
+ if (IsReversed)
+ {
+ selectionStart = GetEndPoint().CurrentPosition;
+ }
+ else
+ {
+ selectionStart = GetStartPoint().CurrentPosition;
+ }
+ }
+
+ this.SelectRange(selectionStart, selectionEnd);
+ }
+
+ private void SelectRange(int selectionStart, int selectionEnd)
+ {
+ SnapshotPoint startPoint = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, selectionStart);
+ SnapshotPoint endPoint = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, selectionEnd);
+
+ TextSelection.Select(new VirtualSnapshotPoint(startPoint), new VirtualSnapshotPoint(endPoint));
+
+ ITextViewLine textViewLine = TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(endPoint);
+ PositionAffinity affinity = (textViewLine.IsLastTextViewLineForSnapshotLine || (endPoint != textViewLine.End)) ? PositionAffinity.Successor : PositionAffinity.Predecessor;
+
+ Caret.MoveTo(endPoint, affinity);
+ TextView.AdvancedTextView.ViewScroller.EnsureSpanVisible(TextSelection.StreamSelectionSpan.SnapshotSpan,
+ (selectionStart <= selectionEnd)
+ ? EnsureSpanVisibleOptions.MinimumScroll
+ : (EnsureSpanVisibleOptions.MinimumScroll | EnsureSpanVisibleOptions.ShowStart));
+ }
+
+ public override void Clear()
+ {
+ TextSelection.Clear();
+ }
+
+ public override ITextSelection AdvancedSelection
+ {
+ get { return TextSelection; }
+ }
+
+ public override bool IsEmpty
+ {
+ get { return TextSelection.IsEmpty; }
+ }
+
+ public override bool IsReversed
+ {
+ get { return TextSelection.IsReversed; }
+ set
+ {
+ if (value)
+ {
+ SelectRange(TextRange.GetEndPoint().CurrentPosition, TextRange.GetStartPoint().CurrentPosition);
+ }
+ else
+ {
+ SelectRange(TextRange.GetStartPoint().CurrentPosition, TextRange.GetEndPoint().CurrentPosition);
+ }
+ }
+ }
+
+ public override TextView TextView
+ {
+ get { return _textView; }
+ }
+
+ public override DisplayTextPoint GetDisplayStartPoint()
+ {
+ return TextRange.GetDisplayStartPoint();
+ }
+
+ public override DisplayTextPoint GetDisplayEndPoint()
+ {
+ return TextRange.GetDisplayEndPoint();
+ }
+
+ public override VisibilityState Visibility
+ {
+ get { return TextRange.Visibility; }
+ }
+
+ protected override DisplayTextRange CloneDisplayTextRangeInternal()
+ {
+ return TextRange.Clone();
+ }
+
+ protected override IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal()
+ {
+ return TextRange.GetEnumerator();
+ }
+
+ public override TextPoint GetStartPoint()
+ {
+ return TextRange.GetStartPoint();
+ }
+
+ public override TextPoint GetEndPoint()
+ {
+ return TextRange.GetEndPoint();
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return TextView.TextBuffer; }
+ }
+
+ public override SnapshotSpan AdvancedTextRange
+ {
+ get { return TextRange.AdvancedTextRange; }
+ }
+
+ public override bool MakeUppercase()
+ {
+ // NOTE: We store this as a *Span* because we don't want it to track
+ Span selectionSpan = TextRange.AdvancedTextRange;
+ bool isReversed = IsReversed;
+ if (!TextRange.MakeUppercase())
+ return false;
+ TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed);
+ return true;
+ }
+
+ public override bool MakeLowercase()
+ {
+ // NOTE: We store this as a *Span* because we don't want it to track
+ Span selectionSpan = TextRange.AdvancedTextRange;
+ bool isReversed = IsReversed;
+ if (!TextRange.MakeLowercase())
+ return false;
+ TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed);
+ return true;
+ }
+
+ public override bool Capitalize()
+ {
+ // NOTE: We store this as a *Span* because we don't want it to track
+ Span selectionSpan = TextRange.AdvancedTextRange;
+ bool isReversed = IsReversed;
+ bool isEmpty = IsEmpty;
+ if (!TextRange.Capitalize())
+ return false;
+
+ if (!isEmpty)
+ {
+ TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed);
+ }
+
+ return true;
+ }
+
+ public override bool ToggleCase()
+ {
+ // NOTE: We store this as a *Span* because we don't want it to track
+ Span selectionSpan = TextRange.AdvancedTextRange;
+ bool isReversed = IsReversed;
+ bool isEmpty = IsEmpty;
+ if (!TextRange.ToggleCase())
+ return false;
+
+ if (!isEmpty)
+ {
+ TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed);
+ }
+
+ return true;
+ }
+
+ public override bool Delete()
+ {
+ foreach (var span in TextSelection.SelectedSpans)
+ {
+ DisplayTextRange selectedRange = TextView.GetTextRange(span.Start, span.End);
+ if (!selectedRange.Delete())
+ return false;
+ }
+
+ return true;
+ }
+
+ public override bool Indent()
+ {
+ bool singleLineSelection = (GetStartPoint().LineNumber == GetEndPoint().LineNumber);
+ bool entireLastLineSelected
+ = (GetStartPoint().CurrentPosition != GetEndPoint().CurrentPosition &&
+ GetStartPoint().CurrentPosition == TextBuffer.GetEndPoint().StartOfLine &&
+ GetEndPoint().CurrentPosition == TextBuffer.GetEndPoint().EndOfLine);
+
+ if (singleLineSelection && !entireLastLineSelected)
+ {
+ TextPoint endPoint = GetEndPoint();
+ if (!Delete())
+ return false;
+ if (!endPoint.InsertIndent())
+ return false;
+ TextView.AdvancedTextView.Caret.MoveTo(endPoint.AdvancedTextPoint);
+ }
+ else // indent the selected lines
+ {
+ VirtualSnapshotPoint oldStartPoint = TextSelection.Start;
+ VirtualSnapshotPoint oldEndPoint = TextSelection.End;
+ bool isReversed = TextSelection.IsReversed;
+
+ ITextSnapshotLine startLine = AdvancedTextRange.Snapshot.GetLineFromPosition(oldStartPoint.Position);
+ ITextSnapshotLine endLine = AdvancedTextRange.Snapshot.GetLineFromPosition(oldEndPoint.Position);
+
+ // If the selection span initially starts at the whitespace at the beginning of the line in the startLine or
+ // ends at the whitespace at the beginning of the line in the endLine, restore selection and caret position,
+ // *unless* the selection was in box mode.
+ bool startAtStartLineWhitespace = oldStartPoint.Position <= _textView.GetTextPoint(startLine.Start).GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition;
+ bool endAtEndLineWhitespace = oldEndPoint.Position < _textView.GetTextPoint(endLine.Start).GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition;
+ bool isBoxSelection = AdvancedSelection.Mode == TextSelectionMode.Box;
+
+ if (isBoxSelection)
+ {
+ if (!this.BoxIndent())
+ return false;
+ }
+ else
+ {
+ if (!TextRange.Indent())
+ return false;
+ }
+
+ // Computing the new selection and caret position
+ VirtualSnapshotPoint newStartPoint = TextSelection.Start;
+ VirtualSnapshotPoint newEndPoint = TextSelection.End;
+
+ if (!isBoxSelection && (startAtStartLineWhitespace || endAtEndLineWhitespace))
+ {
+ // After indent selection span should start at the start of startLine and end at the start of endLine
+ if (startAtStartLineWhitespace)
+ {
+ newStartPoint = new VirtualSnapshotPoint(AdvancedTextRange.Snapshot, oldStartPoint.Position.Position);
+ }
+
+ if (endAtEndLineWhitespace && oldEndPoint.Position.Position != endLine.Start && endLine.Length != 0)
+ {
+ int insertedTextSize = _editorOptions.IsConvertTabsToSpacesEnabled() ? _editorOptions.GetTabSize() : 1;
+ newEndPoint = new VirtualSnapshotPoint(AdvancedTextRange.Snapshot, newEndPoint.Position.Position - insertedTextSize);
+ }
+
+ if (!isReversed)
+ TextSelection.Select(newStartPoint, newEndPoint);
+ else
+ TextSelection.Select(newEndPoint, newStartPoint);
+
+ TextView.AdvancedTextView.Caret.MoveTo(TextSelection.ActivePoint, PositionAffinity.Successor);
+ }
+ }
+ TextView.AdvancedTextView.Caret.EnsureVisible();
+ return true;
+ }
+
+ /// <summary>
+ /// Indent the given box selection
+ /// </summary>
+ /// <remarks>
+ /// This is fairly close to the normal text range indenting logic, except that it also
+ /// indents an empty selection at the endline, which the normal text range ignores.
+ /// </remarks>
+ private bool BoxIndent()
+ {
+ string textToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', _editorOptions.GetTabSize()) : "\t";
+
+ using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int startLineNumber = GetStartPoint().LineNumber;
+ int endLineNumber = GetEndPoint().LineNumber;
+
+ for (int i = startLineNumber; i <= endLineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+ if (line.Length > 0)
+ {
+ if (!edit.Insert(line.Start, textToInsert))
+ return false;
+ }
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+
+ return true;
+ }
+
+ public override bool Unindent()
+ {
+ if (GetStartPoint().LineNumber != GetEndPoint().LineNumber &&
+ AdvancedSelection.Mode == TextSelectionMode.Box)
+ {
+ if (!this.BoxUnindent())
+ return false;
+ }
+ else
+ {
+ if (!TextRange.Unindent())
+ return false;
+ }
+
+ TextView.AdvancedTextView.Caret.EnsureVisible();
+ return true;
+ }
+
+ /// <summary>
+ /// Unindent the given box selection
+ /// </summary>
+ /// <remarks>
+ /// This is fairly close to the normal text range unindenting logic, except that it also
+ /// unindents an empty selection at the endline, which the normal text range ignores.
+ /// </remarks>
+ private bool BoxUnindent()
+ {
+ using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int startLineNumber = GetStartPoint().LineNumber;
+ int endLineNumber = GetEndPoint().LineNumber;
+
+ for (int i = startLineNumber; i <= endLineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+ if (line.Length > 0)
+ {
+ if (snapshot[line.Start] == '\t')
+ {
+ if (!edit.Delete(new Span(line.Start, 1)))
+ return false;
+ }
+ else
+ {
+ int spacesToRemove = 0;
+ for (; (line.Start + spacesToRemove < snapshot.Length) && (spacesToRemove < _editorOptions.GetTabSize());
+ spacesToRemove++)
+ {
+ if (snapshot[line.Start + spacesToRemove] != ' ')
+ {
+ break;
+ }
+ }
+
+ if (spacesToRemove > 0)
+ {
+ if (!edit.Delete(new Span(line.Start, spacesToRemove)))
+ return false;
+ }
+ }
+ }
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+
+ return true;
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ return TextRange.Find(pattern);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ return TextRange.Find(pattern, findOptions);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return TextRange.FindAll(pattern);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ return TextRange.FindAll(pattern, findOptions);
+ }
+
+ public override bool ReplaceText(string newText)
+ {
+ // We use *int* and *Span* here because we don't want them to track
+ int startPosition = TextRange.GetDisplayStartPoint().CurrentPosition;
+ Span newSelectionSpan = new Span(startPosition, newText.Length);
+ bool isReversed = IsReversed;
+ if (!TextRange.ReplaceText(newText))
+ return false;
+ TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, newSelectionSpan), isReversed);
+ return true;
+ }
+
+ public override string GetText()
+ {
+ return TextRange.GetText();
+ }
+
+ public override void SetStart(TextPoint startPoint)
+ {
+ this.SelectRange(startPoint.CurrentPosition, GetDisplayEndPoint().CurrentPosition);
+ }
+
+ public override void SetEnd(TextPoint endPoint)
+ {
+ this.ExtendSelection(endPoint);
+ }
+
+ public override void MoveTo(TextRange newRange)
+ {
+ this.SelectRange(newRange);
+ }
+
+ protected override IEnumerator<TextPoint> GetEnumeratorInternal()
+ {
+ return ((TextRange)TextRange).GetEnumerator();
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs
new file mode 100644
index 0000000..be6d268
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs
@@ -0,0 +1,1015 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Globalization;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+ internal sealed class DefaultTextPointPrimitive : TextPoint
+ {
+ private TextBuffer _textBuffer;
+ private ITrackingPoint _trackingPoint;
+ private IEditorOptions _editorOptions;
+ private ITextStructureNavigator _textStructureNavigator;
+ private ITextSearchService _findLogic;
+ private IBufferPrimitivesFactoryService _bufferPrimitivesFactory;
+
+ public DefaultTextPointPrimitive(
+ TextBuffer textBuffer,
+ int position,
+ ITextSearchService findLogic,
+ IEditorOptions editorOptions,
+ ITextStructureNavigator textStructureNavigator,
+ IBufferPrimitivesFactoryService bufferPrimitivesFactory)
+ {
+ if ((position < 0) ||
+ (position > textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+
+ _textBuffer = textBuffer;
+ _trackingPoint = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.CreateTrackingPoint(0, PointTrackingMode.Positive);
+ _editorOptions = editorOptions;
+ _textStructureNavigator = textStructureNavigator;
+ _findLogic = findLogic;
+ _bufferPrimitivesFactory = bufferPrimitivesFactory;
+
+ MoveTo(position);
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _textBuffer; }
+ }
+
+ public override int CurrentPosition
+ {
+ get { return _trackingPoint.GetPosition(_textBuffer.AdvancedTextBuffer.CurrentSnapshot); }
+ }
+
+ public override int Column
+ {
+ get
+ {
+ SnapshotPoint currentLocation = _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot);
+
+ ITextSnapshotLine line = currentLocation.GetContainingLine();
+
+ if (line.Start != currentLocation.Position)
+ {
+ string lineText = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetText(Span.FromBounds(line.Start, currentLocation.Position));
+
+ StringInfo lineTextInfo = new StringInfo(lineText);
+
+ int column = 0;
+ for (int i = 0; i < lineTextInfo.LengthInTextElements; i++)
+ {
+ string textElement = lineTextInfo.SubstringByTextElements(i, 1);
+ if (textElement == "\t")
+ {
+ // If there is a tab in the text, then the column automatically jumps
+ // to the next tab stop.
+ int tabSize = _editorOptions.GetTabSize();
+ column = ((column / tabSize) + 1) * tabSize;
+ }
+ else
+ {
+ column++;
+ }
+ }
+
+ return column;
+ }
+
+ return 0;
+ }
+ }
+
+ public override bool DeleteNext()
+ {
+ string nextCharacter = GetNextCharacter();
+ return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, CurrentPosition, nextCharacter.Length);
+ }
+
+ public override bool DeletePrevious()
+ {
+ ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int previousPosition = CurrentPosition - 1;
+
+ if (previousPosition < 0)
+ {
+ return true;
+ }
+
+ int index = previousPosition;
+ char currentCharacter = snapshot[previousPosition];
+
+ // By default VS (and many other apps) will delete only the last character
+ // of a combining character sequence. The one exception to this rule is
+ // surrogate pais which we are handling here.
+ if (char.GetUnicodeCategory(currentCharacter) == UnicodeCategory.Surrogate)
+ {
+ index--;
+ }
+
+ if (index > 0)
+ {
+ if (currentCharacter == '\n')
+ {
+ if (snapshot[previousPosition - 1] == '\r')
+ {
+ index--;
+ }
+ }
+ }
+
+ return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, index, previousPosition - index + 1);
+ }
+
+ public override TextRange GetCurrentWord()
+ {
+ // If the line is blank, just return a blank range for the line
+ SnapshotPoint currentPoint = AdvancedTextPoint;
+ ITextSnapshotLine line = currentPoint.GetContainingLine();
+ if (currentPoint == line.Start && line.Length == 0)
+ {
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this, this);
+ }
+
+ TextExtent textExtent = GetTextExtent(CurrentPosition);
+
+ // If the word is not significant, then see if there is a significant word right before this one,
+ // and return that instead to mimic VS 9 behavior.
+ if (!textExtent.IsSignificant)
+ {
+ if ((textExtent.Span.Start > 0) && (textExtent.Span.Length > 0))
+ {
+ if (textExtent.Span.Start == CurrentPosition)
+ {
+ if (!char.IsWhiteSpace(_textBuffer.AdvancedTextBuffer.CurrentSnapshot[textExtent.Span.Start - 1]))
+ {
+ textExtent = GetTextExtent(textExtent.Span.Start - 1);
+ }
+ }
+ else if (CurrentPosition == EndOfLine)
+ {
+ // If this text point is on the end of the line, then check to see if there is a word
+ // just before the whitespace.
+ if (!char.IsWhiteSpace(_textBuffer.AdvancedTextBuffer.CurrentSnapshot[textExtent.Span.Start - 1]))
+ {
+ TextExtent newExtent = new TextExtent(GetTextExtent(textExtent.Span.Start - 1));
+ textExtent = new TextExtent(new SnapshotSpan(newExtent.Span.Start, textExtent.Span.End), true);
+ }
+ }
+ }
+ }
+
+ TextPoint startPoint = Clone();
+ startPoint.MoveTo(textExtent.Span.Start);
+ TextPoint endPoint = Clone();
+ endPoint.MoveTo(textExtent.Span.End);
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, startPoint, endPoint);
+ }
+
+ private TextExtent GetTextExtent(int position)
+ {
+ return _textStructureNavigator.GetExtentOfWord(new SnapshotPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot, position));
+ }
+
+ public override TextRange GetNextWord()
+ {
+ TextExtent currentWord = GetTextExtent(CurrentPosition);
+
+ if (currentWord.Span.End < _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)
+ {
+ // If the current point is at the end of the line, look for the next word on the next line
+ if ((CurrentPosition == EndOfLine) && (CurrentPosition != _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length))
+ {
+ TextPoint textPoint = Clone();
+ textPoint.MoveToBeginningOfNextLine();
+ textPoint.MoveTo(textPoint.StartOfLine);
+ TextExtent wordOnNextLine = GetTextExtent(textPoint.CurrentPosition);
+ if (wordOnNextLine.IsSignificant)
+ {
+ return textPoint.GetCurrentWord();
+ }
+ else if (wordOnNextLine.Span.End >= textPoint.EndOfLine)
+ {
+ return textPoint.GetTextRange(textPoint.EndOfLine);
+ }
+ return textPoint.GetNextWord();
+ }
+ // By default, VS stops at line breaks when determing word
+ // boundaries.
+ else if (ShouldStopAtEndOfLine(currentWord.Span.End) || IsCurrentWordABlankLine(currentWord))
+ {
+ return GetTextRange(currentWord.Span.End);
+ }
+
+ TextExtent nextWord = GetTextExtent(currentWord.Span.End);
+
+ if (!nextWord.IsSignificant)
+ {
+ nextWord = GetTextExtent(nextWord.Span.End);
+ }
+
+ int start = nextWord.Span.Start;
+ int end = nextWord.Span.End;
+
+ // The text structure navigator can return a word with whitespace attached at the end.
+ // Handle that case here.
+ start = Math.Max(start, currentWord.Span.End);
+
+ TextPoint startPoint = Clone();
+ TextPoint endPoint = Clone();
+ startPoint.MoveTo(start);
+ endPoint.MoveTo(end);
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, startPoint, endPoint);
+ }
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, TextBuffer.GetEndPoint(), TextBuffer.GetEndPoint());
+ }
+
+ public override TextRange GetPreviousWord()
+ {
+ TextRange currentWord = GetCurrentWord();
+
+ if (currentWord.GetStartPoint().CurrentPosition > 0)
+ {
+ // By default, VS stops at line breaks when determing word
+ // boundaries.
+ if ((currentWord.GetStartPoint().CurrentPosition == StartOfLine) &&
+ (CurrentPosition != StartOfLine))
+ {
+ return GetTextRange(currentWord.GetStartPoint());
+ }
+
+ // If the point is at the end of a word that is not whitespace, it is possible
+ // that the "current word" is also the previous word in standard VS.
+ if ((currentWord.GetEndPoint().CurrentPosition == CurrentPosition) &&
+ (!currentWord.IsEmpty))
+ {
+ return currentWord;
+ }
+
+ TextPoint pointInPreviousWord = currentWord.GetStartPoint();
+ pointInPreviousWord.MoveTo(pointInPreviousWord.CurrentPosition - 1);
+
+ TextRange previousWord = pointInPreviousWord.GetCurrentWord();
+
+ if (previousWord.GetStartPoint().CurrentPosition > 0)
+ {
+ if (ShouldContinuePastPreviousWord(previousWord))
+ {
+ pointInPreviousWord.MoveTo(previousWord.GetStartPoint().CurrentPosition - 1);
+ previousWord = pointInPreviousWord.GetCurrentWord();
+ }
+ }
+
+ return previousWord;
+ }
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, TextBuffer.GetStartPoint(), TextBuffer.GetStartPoint());
+ }
+
+ public override TextRange GetTextRange(TextPoint otherPoint)
+ {
+ if (otherPoint == null)
+ {
+ throw new ArgumentNullException("otherPoint");
+ }
+
+ if (otherPoint.TextBuffer != TextBuffer)
+ {
+ throw new ArgumentException(Strings.OtherPointFromWrongBuffer);
+ }
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), otherPoint);
+ }
+
+ public override TextRange GetTextRange(int otherPosition)
+ {
+ if ((otherPosition < 0) || (otherPosition > TextBuffer.AdvancedTextBuffer.CurrentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("otherPosition");
+ }
+
+ TextPoint otherPoint = this.Clone();
+ otherPoint.MoveTo(otherPosition);
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), otherPoint);
+ }
+
+ public override bool InsertNewLine()
+ {
+ string lineBreak = null;
+ if (_editorOptions.GetReplicateNewLineCharacter())
+ {
+ ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int position = _trackingPoint.GetPosition(snapshot);
+ ITextSnapshotLine currentSnapshotLine = snapshot.GetLineFromPosition(position);
+ if (currentSnapshotLine.LineBreakLength > 0)
+ {
+ // use the same line ending as the current line
+ lineBreak = currentSnapshotLine.GetLineBreakText();
+ }
+ else
+ {
+ // we are on the last line of the buffer
+ if (snapshot.LineCount > 1)
+ {
+ // use the same line ending as the penultimate line in the buffer
+ lineBreak = snapshot.GetLineFromLineNumber(snapshot.LineCount - 2).GetLineBreakText();
+ }
+ }
+ }
+ return InsertText(lineBreak ?? _editorOptions.GetNewLineCharacter());
+ }
+
+ public override bool InsertIndent()
+ {
+ int tabSize = _editorOptions.GetTabSize();
+ int spacesToInsert = tabSize - (Column % tabSize);
+ string indentToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', spacesToInsert) : "\t";
+
+ return InsertText(indentToInsert);
+ }
+
+ public override bool InsertText(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ if (text.Length > 0)
+ {
+ return PrimitivesUtilities.Insert(_textBuffer.AdvancedTextBuffer, _trackingPoint.GetPosition(_textBuffer.AdvancedTextBuffer.CurrentSnapshot), text);
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ public override int LineNumber
+ {
+ get
+ {
+ return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().LineNumber;
+ }
+ }
+
+ public override int StartOfLine
+ {
+ get
+ {
+ return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().Start;
+ }
+ }
+
+ public override int EndOfLine
+ {
+ get
+ {
+ return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().End;
+ }
+ }
+
+ public override bool RemovePreviousIndent()
+ {
+ if (Column > 0)
+ {
+ int tabSize = _editorOptions.GetTabSize();
+
+ int previousTabStop = Column - tabSize;
+ if (Column % tabSize > 0)
+ {
+ previousTabStop = (Column / tabSize) * tabSize;
+ }
+
+ int positionToDeleteTo = CurrentPosition;
+
+ TextPoint newPoint = Clone();
+ for (int i = CurrentPosition - 1; newPoint.Column >= previousTabStop; i--)
+ {
+ newPoint.MoveTo(i);
+ string character = newPoint.GetNextCharacter();
+ if (character != " " && character != "\t")
+ {
+ break;
+ }
+
+ positionToDeleteTo = i;
+
+ if (newPoint.Column == previousTabStop)
+ {
+ break;
+ }
+ }
+
+ return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, Span.FromBounds(positionToDeleteTo, CurrentPosition));
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ public override bool TransposeCharacter()
+ {
+ int insertionIndex = CurrentPosition;
+
+ ITextSnapshotLine line = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromPosition(insertionIndex);
+
+ string lineText = line.GetText();
+
+ if (StringInfo.ParseCombiningCharacters(lineText).Length < 2)
+ {
+ return true;
+ }
+
+ Span textElementLeftSpan, textElementRightSpan;
+ string textElementLeft, textElementRight;
+ int linePosition = CurrentPosition - StartOfLine;
+
+ // We're at the start of a line
+ if (insertionIndex == line.Start)
+ {
+ textElementLeft = StringInfo.GetNextTextElement(lineText, linePosition);
+ textElementLeftSpan = new Span(linePosition + line.Start, textElementLeft.Length);
+ textElementRight = StringInfo.GetNextTextElement(lineText, linePosition + textElementLeft.Length);
+ textElementRightSpan = new Span(textElementLeftSpan.End, textElementRight.Length);
+ }
+ // We're at the end of a line
+ else if (insertionIndex == line.End)
+ {
+ textElementRight = StringInfo.GetNextTextElement(lineText, linePosition - 1);
+ textElementRightSpan = new Span(linePosition - 1 + line.Start, textElementRight.Length);
+ textElementLeft = StringInfo.GetNextTextElement(lineText, textElementRightSpan.Start - line.Start - 1);
+ textElementLeftSpan = new Span(textElementRightSpan.Start - 1, textElementRight.Length);
+ }
+ // We're at the middle of a line
+ else
+ {
+ textElementRight = StringInfo.GetNextTextElement(lineText, linePosition);
+ textElementRightSpan = new Span(linePosition + line.Start, textElementRight.Length);
+ textElementLeft = StringInfo.GetNextTextElement(lineText, textElementRightSpan.Start - line.Start - 1);
+ textElementLeftSpan = new Span(textElementRightSpan.Start - 1, textElementRight.Length);
+ }
+
+ string transposedText = textElementRight + textElementLeft;
+
+ return PrimitivesUtilities.Replace(_textBuffer.AdvancedTextBuffer, new Span(textElementLeftSpan.Start, transposedText.Length), transposedText);
+ }
+
+ public override bool TransposeLine()
+ {
+ ITextSnapshot currentSnapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+
+ // If there is only a single line in this buffer, don't do anything.
+ if (currentSnapshot.LineCount <= 1)
+ {
+ return true;
+ }
+
+ ITextSnapshotLine currentLine = currentSnapshot.GetLineFromPosition(CurrentPosition);
+
+ ITextSnapshotLine nextLine = null;
+
+ if (currentLine.LineNumber == currentSnapshot.LineCount - 1)
+ {
+ // If the caret is on the last line of the buffer, transpose the next to last line
+ // with the last one.
+ nextLine = currentSnapshot.GetLineFromLineNumber(currentLine.LineNumber - 1);
+ }
+ else
+ {
+ nextLine = currentSnapshot.GetLineFromLineNumber(currentLine.LineNumber + 1);
+ }
+
+ return TransposeLine(nextLine.LineNumber);
+ }
+
+ public override bool TransposeLine(int lineNumber)
+ {
+ if ((lineNumber < 0) || (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount))
+ {
+ throw new ArgumentOutOfRangeException("lineNumber");
+ }
+
+ ITextSnapshot currentSnapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+
+ // If there is only a single line in this buffer, don't do anything.
+ if (currentSnapshot.LineCount <= 1)
+ {
+ return true;
+ }
+
+ ITextSnapshotLine currentLine = currentSnapshot.GetLineFromPosition(CurrentPosition);
+ ITextSnapshotLine nextLine = currentSnapshot.GetLineFromLineNumber(lineNumber);
+
+ using (ITextEdit edit = _textBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ // Make sure that both replaces succeed before applying
+ if ((edit.Replace(currentLine.Extent, nextLine.GetText())) &&
+ (edit.Replace(nextLine.Extent, currentLine.GetText())))
+ {
+ edit.Apply();
+ if (edit.Canceled)
+ return false;
+ }
+ else
+ {
+ edit.Cancel();
+ return false;
+ }
+ }
+
+ int newLineNumber = nextLine.LineNumber;
+ if (currentLine.LineNumber == currentSnapshot.LineCount - 1)
+ {
+ newLineNumber = currentLine.LineNumber;
+ }
+
+ MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(newLineNumber).Start);
+
+ return true;
+ }
+
+ public override SnapshotPoint AdvancedTextPoint
+ {
+ get { return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot); }
+ }
+
+ public override string GetNextCharacter()
+ {
+ ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int currentPosition = CurrentPosition;
+
+ if (currentPosition == snapshot.Length)
+ {
+ return string.Empty;
+ }
+
+ int index = currentPosition;
+ string characterText = char.ToString(snapshot[currentPosition]);
+
+ while (++index < snapshot.Length)
+ {
+ string newText = characterText + snapshot[index];
+ if (StringInfo.GetNextTextElement(newText).Length <= characterText.Length)
+ break;
+
+ characterText = newText;
+ }
+
+ return characterText;
+ }
+
+ public override string GetPreviousCharacter()
+ {
+ ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ int currentPosition = CurrentPosition;
+
+ if (currentPosition == 0)
+ {
+ return string.Empty;
+ }
+
+ int index = currentPosition - 1;
+ string characterText = char.ToString(snapshot[index]);
+
+ while (--index >= 0)
+ {
+ string newText = snapshot[index] + characterText;
+ if (StringInfo.GetNextTextElement(newText).Length <= characterText.Length)
+ break;
+
+ characterText = newText;
+ }
+
+ return characterText;
+ }
+
+ public override TextRange Find(string pattern, TextPoint endPoint)
+ {
+ ValidateFindParameters(pattern, endPoint);
+ return Find(pattern, FindOptions.None, endPoint);
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ ValidateFindParameters(pattern, this);
+ return Find(pattern, FindOptions.None);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ ValidateFindParameters(pattern, this);
+ return FindMatch(pattern, findOptions, null);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ ValidateFindParameters(pattern, endPoint);
+ return FindMatch(pattern, findOptions, endPoint);
+ }
+
+ private TextRange FindMatch(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ if (pattern.Length == 0)
+ {
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), this.Clone());
+ }
+
+ FindData findData = new FindData(pattern, _textBuffer.AdvancedTextBuffer.CurrentSnapshot);
+ findData.FindOptions = findOptions;
+
+ bool wrapAround = endPoint == null;
+
+ bool searchReverse = ((findData.FindOptions & FindOptions.SearchReverse) == FindOptions.SearchReverse);
+
+ // In the case of a wrap-around search, we can start the search at this point always. In the case
+ // where we are searching a range in reverse, we have to start at the end point.
+ int searchStartPosition = (searchReverse && !wrapAround) ? endPoint.CurrentPosition : CurrentPosition;
+
+ SnapshotSpan? snapshotSpan = _findLogic.FindNext(searchStartPosition, wrapAround, findData);
+
+ if (snapshotSpan.HasValue)
+ {
+ if ((endPoint == null) || (snapshotSpan.Value.End <= endPoint.CurrentPosition))
+ {
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer,
+ _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, snapshotSpan.Value.Start),
+ _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, snapshotSpan.Value.End));
+ }
+ }
+
+ return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), this.Clone());
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint)
+ {
+ return FindAll(pattern, FindOptions.None, endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return FindAll(pattern, FindOptions.None);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ ValidateFindParameters(pattern, this);
+
+ if (pattern.Length == 0)
+ {
+ return new Collection<TextRange>();
+ }
+
+ FindData findData = new FindData(pattern, _textBuffer.AdvancedTextBuffer.CurrentSnapshot);
+ findData.FindOptions = findOptions;
+
+ // Assume that FindAll returns matches sorted starting at the beginning
+ // of the snapshot.
+ Collection<SnapshotSpan> matches = _findLogic.FindAll(findData);
+
+ List<TextRange> ranges = new List<TextRange>();
+ Collection<TextRange> beforeRanges = new Collection<TextRange>();
+
+ foreach (SnapshotSpan span in matches)
+ {
+ TextRange textRange = _bufferPrimitivesFactory.CreateTextRange(_textBuffer,
+ _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, span.Start),
+ _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, span.End));
+
+ if (textRange.GetStartPoint().CurrentPosition < CurrentPosition)
+ {
+ beforeRanges.Add(textRange);
+ }
+ else
+ {
+ ranges.Add(textRange);
+ }
+ }
+
+ ranges.AddRange(beforeRanges);
+
+ return new Collection<TextRange>(ranges);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint)
+ {
+ ValidateFindParameters(pattern, endPoint);
+
+ Collection<TextRange> matchingRanges = new Collection<TextRange>();
+ Collection<TextRange> textRanges = FindAll(pattern, findOptions);
+
+ foreach (TextRange textRange in textRanges)
+ {
+ if ((textRange.GetStartPoint().CurrentPosition >= CurrentPosition) &&
+ (textRange.GetEndPoint().CurrentPosition <= endPoint.CurrentPosition))
+ {
+ matchingRanges.Add(textRange);
+ }
+ }
+
+ return matchingRanges;
+ }
+
+ private void ValidateFindParameters(string pattern, TextPoint endPoint)
+ {
+ if (pattern == null)
+ {
+ throw new ArgumentNullException("pattern");
+ }
+ if (endPoint == null)
+ {
+ throw new ArgumentNullException("endPoint");
+ }
+ if (endPoint.TextBuffer != TextBuffer)
+ {
+ throw new ArgumentException(Strings.OtherPointFromWrongBuffer);
+ }
+ }
+
+ public override void MoveTo(int position)
+ {
+ position = FindStartOfCombiningSequence(_textBuffer.AdvancedTextBuffer.CurrentSnapshot, position);
+
+ _trackingPoint = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.CreateTrackingPoint(position, PointTrackingMode.Positive);
+ }
+
+ /// <summary>
+ /// Given a snapshot and position, find the start of the first combining sequence that is less
+ /// than or equal to the given position. In the 99% case, the position is already at the start
+ /// of a sequence, but we want to make sure it isn't in the middle of a surrogate pair or between
+ /// a base character/surrogate pair and its associated combining characters (which may also
+ /// be surrogate pairs).
+ /// </summary>
+ /// <returns>The start of position of the combining sequence, usually <paramref name="position"/> again.</returns>
+ private static int FindStartOfCombiningSequence(ITextSnapshot snapshot, int position)
+ {
+ if ((position < 0) ||
+ (position > snapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+
+ // If this is the end of the snapshot, we don't need to check anything.
+ if (position == snapshot.Length)
+ return position;
+
+ // 99% case:
+ // Most of the time, the given character is neither a combining character nor a surrogate codepoint.
+ // In that case, we don't need to walk around the snapshot looking for the start of a combining
+ // character sequence.
+ UnicodeCategory categoryAtPosition = CharUnicodeInfo.GetUnicodeCategory(snapshot[position]);
+ if (!IsPotentialCombiningCharacter(categoryAtPosition))
+ return position;
+
+ // If we're at the start of a line, we're already in a good position.
+ var line = snapshot.GetLineFromPosition(position);
+ if (position == line.Start)
+ return position;
+
+ // If we've made it all the way here, we have a potential combining character sequence.
+ // We've already determined that the current character is a potential combining character,
+ // so start at one character previous.
+ int endPosition = position + 1;
+ int lineStart = line.Start;
+ position--;
+
+ for (; position > lineStart; position--)
+ {
+ char ch = snapshot[position];
+ UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(ch);
+
+ // We can skip potential combining characters (we'll gather them all up once we
+ // hit a base character).
+ if (IsPotentialCombiningCharacter(category))
+ {
+ continue;
+ }
+
+ // At this point, the character is likely a base. Use ParseCombiningCharacters to find the sequences
+ // in the text up to this point.
+
+ string upToCurrentPosition = snapshot.GetText(Span.FromBounds(position, endPosition));
+ int[] sequences = StringInfo.ParseCombiningCharacters(upToCurrentPosition);
+
+ // If we've found more than one combining character sequence, use the last one as our
+ // position and stop looping.
+ if (sequences.Length > 1)
+ {
+ return (position) + sequences[sequences.Length - 1];
+ }
+ }
+
+ // If we walked the position back to the start of the line, do one last combining character sequence check and use the result of that
+ if (position == line.Start)
+ {
+ string upToCurrentPosition = snapshot.GetText(Span.FromBounds(position, endPosition));
+ int[] sequences = StringInfo.ParseCombiningCharacters(upToCurrentPosition);
+ if (sequences.Length > 1)
+ {
+ return position + sequences[sequences.Length - 1];
+ }
+ }
+
+ return position;
+ }
+
+ /// <summary>
+ /// Determine if the character is potentially a part of a combining sequence. This includes
+ /// the known combining character classes (M) and surrogates, as the MSDN documentation for
+ /// .NET Unicode support notes that it may consider surrogates to be combining characters.
+ /// </summary>
+ private static bool IsPotentialCombiningCharacter(UnicodeCategory category)
+ {
+ return category == UnicodeCategory.EnclosingMark ||
+ category == UnicodeCategory.NonSpacingMark ||
+ category == UnicodeCategory.SpacingCombiningMark ||
+ category == UnicodeCategory.Surrogate;
+ }
+
+ public override void MoveToNextCharacter()
+ {
+ string nextCharacter = GetNextCharacter();
+ MoveTo(CurrentPosition + nextCharacter.Length);
+ }
+
+ public override void MoveToPreviousCharacter()
+ {
+ string previousCharacter = GetPreviousCharacter();
+
+ MoveTo(CurrentPosition - previousCharacter.Length);
+ }
+
+ protected override TextPoint CloneInternal()
+ {
+ return new DefaultTextPointPrimitive(_textBuffer, CurrentPosition, _findLogic, _editorOptions, _textStructureNavigator, _bufferPrimitivesFactory);
+ }
+
+ public override void MoveToLine(int lineNumber)
+ {
+ if ((lineNumber < 0) ||
+ (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount))
+ {
+ throw new ArgumentOutOfRangeException("lineNumber");
+ }
+
+ ITextSnapshotLine line = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber);
+
+ MoveTo(line.Start);
+ }
+
+ public override void MoveToEndOfLine()
+ {
+ MoveTo(EndOfLine);
+ }
+
+ public override void MoveToStartOfLine()
+ {
+ int firstNonWhitespaceCharacter = GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition;
+ int currentPosition = CurrentPosition;
+ int startOfLine = StartOfLine;
+ if ((currentPosition == firstNonWhitespaceCharacter) ||
+ (firstNonWhitespaceCharacter == EndOfLine))
+ {
+ MoveTo(startOfLine);
+ }
+ else if ((currentPosition > firstNonWhitespaceCharacter) ||
+ (currentPosition == 0))
+ {
+ MoveTo(firstNonWhitespaceCharacter);
+ }
+ }
+
+ public override void MoveToEndOfDocument()
+ {
+ MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length);
+ }
+
+ public override void MoveToStartOfDocument()
+ {
+ MoveTo(0);
+ }
+
+ public override void MoveToBeginningOfNextLine()
+ {
+ MoveTo(AdvancedTextPoint.GetContainingLine().EndIncludingLineBreak);
+ }
+
+ public override void MoveToBeginningOfPreviousLine()
+ {
+ ITextSnapshotLine line = AdvancedTextPoint.GetContainingLine();
+
+ if (line.LineNumber != 0)
+ {
+ MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(line.LineNumber - 1).Start);
+ }
+ else
+ {
+ MoveToStartOfLine();
+ }
+ }
+
+ public override void MoveToNextWord()
+ {
+ if (CurrentPosition != _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)
+ {
+ TextRange nextWord = GetNextWord();
+
+ if (nextWord.GetStartPoint().CurrentPosition == CurrentPosition)
+ {
+ MoveTo(nextWord.GetEndPoint().CurrentPosition);
+ }
+ else
+ {
+ MoveTo(nextWord.GetStartPoint().CurrentPosition);
+ }
+ }
+ }
+
+ public override void MoveToPreviousWord()
+ {
+ if (CurrentPosition != 0)
+ {
+ TextRange currentWord = GetCurrentWord();
+ if (CurrentPosition != currentWord.GetStartPoint().CurrentPosition)
+ {
+ MoveTo(currentWord.GetStartPoint().CurrentPosition);
+ }
+ else
+ {
+ TextRange previousWord = GetPreviousWord();
+
+ MoveTo(previousWord.GetStartPoint().CurrentPosition);
+ }
+ }
+ }
+
+ public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine()
+ {
+ int firstNonWhitespaceCharacterOnLine = StartOfLine;
+ for (; firstNonWhitespaceCharacterOnLine < EndOfLine; firstNonWhitespaceCharacterOnLine++)
+ {
+ if (!char.IsWhiteSpace(AdvancedTextPoint.Snapshot[firstNonWhitespaceCharacterOnLine]))
+ {
+ break;
+ }
+ }
+ return _bufferPrimitivesFactory.CreateTextPoint(TextBuffer, firstNonWhitespaceCharacterOnLine);
+ }
+
+ /// <summary>
+ /// Determine if this text point's current word is a blank line on the next line.
+ /// </summary>
+ /// <param name="currentWord">The current word to check.</param>
+ private bool IsCurrentWordABlankLine(TextExtent currentWord)
+ {
+ TextPoint endOfCurrentWord = Clone();
+ endOfCurrentWord.MoveTo(currentWord.Span.End);
+ return (EndOfLine == CurrentPosition) && (currentWord.Span.End == endOfCurrentWord.EndOfLine);
+ }
+
+ /// <summary>
+ /// Determine if a the next word should stop at the end of the line.
+ /// </summary>
+ private bool ShouldStopAtEndOfLine(int endOfWord)
+ {
+ // If the current word ends at the end of the line and the current position is not the end of the line,
+ // then the word movement should stop at the end of the line.
+ return (endOfWord == EndOfLine) && (EndOfLine > CurrentPosition);
+ }
+
+ /// <summary>
+ /// Determine if the checking of the previous word should go past the previous word.
+ /// </summary>
+ /// <param name="previousWord">The current previous word.</param>
+ private static bool ShouldContinuePastPreviousWord(TextRange previousWord)
+ {
+ // If the previous word is whitespace, and the previous word is not a blank line
+ // then it should be included in the previous word.
+ return char.IsWhiteSpace(previousWord.GetStartPoint().GetNextCharacter()[0]) &&
+ previousWord.GetStartPoint().CurrentPosition != previousWord.GetStartPoint().StartOfLine;
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs
new file mode 100644
index 0000000..961db4f
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs
@@ -0,0 +1,380 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Globalization;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
+
+ internal sealed class DefaultTextRangePrimitive : TextRange
+ {
+ private TextPoint _startPoint;
+ private TextPoint _endPoint;
+ private IEditorOptions _editorOptions;
+ private IEditorOptionsFactoryService _editorOptionsProvider;
+
+ internal DefaultTextRangePrimitive(TextPoint startPoint, TextPoint endPoint, IEditorOptionsFactoryService editorOptionsProvider)
+ {
+ if (startPoint.CurrentPosition < endPoint.CurrentPosition)
+ {
+ _startPoint = startPoint.Clone();
+ _endPoint = endPoint.Clone();
+ }
+ else
+ {
+ _endPoint = startPoint.Clone();
+ _startPoint = endPoint.Clone();
+ }
+ _editorOptionsProvider = editorOptionsProvider;
+ _editorOptions = _editorOptionsProvider.GetOptions(_startPoint.TextBuffer.AdvancedTextBuffer);
+ }
+
+ public override TextPoint GetStartPoint()
+ {
+ return _startPoint.Clone();
+ }
+
+ public override TextPoint GetEndPoint()
+ {
+ return _endPoint.Clone();
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _startPoint.TextBuffer; }
+ }
+
+ public override SnapshotSpan AdvancedTextRange
+ {
+ get { return new SnapshotSpan(TextBuffer.AdvancedTextBuffer.CurrentSnapshot, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition)); }
+ }
+
+ public override bool IsEmpty
+ {
+ get { return _startPoint.CurrentPosition == _endPoint.CurrentPosition; }
+ }
+
+ public override bool MakeUppercase()
+ {
+ return ReplaceText(GetText().ToUpper(CultureInfo.CurrentCulture));
+ }
+
+ public override bool MakeLowercase()
+ {
+ return ReplaceText(GetText().ToLower(CultureInfo.CurrentCulture));
+ }
+
+ public override bool Capitalize()
+ {
+ int startPosition = _startPoint.CurrentPosition;
+ if (IsEmpty)
+ {
+ int endPosition = _endPoint.CurrentPosition;
+ TextRange currentWord = _startPoint.GetCurrentWord();
+ string nextCharacter = _startPoint.GetNextCharacter();
+ if (_startPoint.CurrentPosition == currentWord.GetStartPoint().CurrentPosition)
+ {
+ nextCharacter = nextCharacter.ToUpper(CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ nextCharacter = nextCharacter.ToLower(CultureInfo.CurrentCulture);
+ }
+ if (!PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, new Span(_startPoint.CurrentPosition, nextCharacter.Length), nextCharacter))
+ return false;
+ _endPoint.MoveTo(endPosition);
+ }
+ else
+ {
+ using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ TextRange currentWord = _startPoint.GetCurrentWord();
+
+ // If the current word extends past this range, go to the next word
+ if (currentWord.GetStartPoint().CurrentPosition < _startPoint.CurrentPosition)
+ {
+ currentWord = currentWord.GetEndPoint().GetNextWord();
+ }
+
+ while (currentWord.GetStartPoint().CurrentPosition < _endPoint.CurrentPosition)
+ {
+ string wordText = currentWord.GetText();
+ string startElement = StringInfo.GetNextTextElement(wordText);
+ wordText = startElement.ToUpper(CultureInfo.CurrentCulture) + wordText.Substring(startElement.Length).ToLower(CultureInfo.CurrentCulture);
+ if (!edit.Replace(currentWord.AdvancedTextRange.Span, wordText))
+ {
+ edit.Cancel();
+ return false;
+ }
+
+ currentWord = currentWord.GetEndPoint().GetNextWord();
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+ }
+ _startPoint.MoveTo(startPosition);
+ return true;
+ }
+
+ public override bool ToggleCase()
+ {
+ if (IsEmpty)
+ {
+ TextPoint nextPoint = _startPoint.Clone();
+ nextPoint.MoveToNextCharacter();
+ TextRange nextCharacter = _startPoint.GetTextRange(nextPoint);
+ string nextCharacterString = nextCharacter.GetText();
+ if (char.IsUpper(nextCharacterString, 0))
+ {
+ nextCharacterString = nextCharacterString.ToLower(CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ nextCharacterString = nextCharacterString.ToUpper(CultureInfo.CurrentCulture);
+ }
+ return nextCharacter.ReplaceText(nextCharacterString);
+ }
+ else
+ {
+ int startPosition = _startPoint.CurrentPosition;
+ using (ITextEdit textEdit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ for (int i = _startPoint.CurrentPosition; i < _endPoint.CurrentPosition; i++)
+ {
+ char newChar = textEdit.Snapshot[i];
+ if (char.IsUpper(newChar))
+ {
+ newChar = char.ToLower(newChar, CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ newChar = char.ToUpper(newChar, CultureInfo.CurrentCulture);
+ }
+
+ if (!textEdit.Replace(i, 1, newChar.ToString()))
+ {
+ textEdit.Cancel();
+ return false; // break out early if any edit fails to reduce the time of the failure case
+ }
+ }
+
+ textEdit.Apply();
+
+ if (textEdit.Canceled)
+ return false;
+ }
+ _startPoint.MoveTo(startPosition);
+ }
+ return true;
+ }
+
+ public override bool Delete()
+ {
+ return PrimitivesUtilities.Delete(TextBuffer.AdvancedTextBuffer, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition));
+ }
+
+ public override bool Indent()
+ {
+ string textToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', _editorOptions.GetTabSize()) : "\t";
+
+ if (_startPoint.LineNumber == _endPoint.LineNumber)
+ {
+ return _startPoint.InsertIndent();
+ }
+
+ using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot;
+ for (int i = _startPoint.LineNumber; i <= _endPoint.LineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+ if ((line.Length > 0) &&
+ (line.Start != _endPoint.CurrentPosition))
+ {
+ if (!edit.Insert(line.Start, textToInsert))
+ return false;
+ }
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+
+ return true;
+ }
+
+ public override bool Unindent()
+ {
+ if (_startPoint.LineNumber == _endPoint.LineNumber)
+ {
+ return _startPoint.RemovePreviousIndent();
+ }
+
+ using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit())
+ {
+ ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot;
+
+ for (int i = _startPoint.LineNumber; i <= _endPoint.LineNumber; i++)
+ {
+ ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i);
+ if ((line.Length > 0) && (_endPoint.CurrentPosition != line.Start))
+ {
+ if (snapshot[line.Start] == '\t')
+ {
+ if (!edit.Delete(new Span(line.Start, 1)))
+ return false;
+ }
+ else
+ {
+ int spacesToRemove = 0;
+ for (; (line.Start + spacesToRemove < snapshot.Length) && (spacesToRemove < _editorOptions.GetTabSize());
+ spacesToRemove++)
+ {
+ if (snapshot[line.Start + spacesToRemove] != ' ')
+ {
+ break;
+ }
+ }
+
+ if (spacesToRemove > 0)
+ {
+ if (!edit.Delete(new Span(line.Start, spacesToRemove)))
+ return false;
+ }
+ }
+ }
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+
+ return true;
+ }
+
+ public override TextRange Find(string pattern)
+ {
+ return Find(pattern, FindOptions.None);
+ }
+
+ public override TextRange Find(string pattern, FindOptions findOptions)
+ {
+ return _startPoint.Find(pattern, findOptions, _endPoint);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern)
+ {
+ return FindAll(pattern, FindOptions.None);
+ }
+
+ public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions)
+ {
+ return _startPoint.FindAll(pattern, findOptions, _endPoint);
+ }
+
+ public override bool ReplaceText(string newText)
+ {
+ if (string.IsNullOrEmpty(newText))
+ {
+ throw new ArgumentNullException("newText");
+ }
+
+ int startPoint = _startPoint.CurrentPosition;
+
+ if (!PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition), newText))
+ return false;
+
+ _startPoint.MoveTo(startPoint);
+
+ return true;
+ }
+
+ public override string GetText()
+ {
+ return TextBuffer.AdvancedTextBuffer.CurrentSnapshot.GetText(Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition));
+ }
+
+ protected override TextRange CloneInternal()
+ {
+ return new DefaultTextRangePrimitive(_startPoint, _endPoint, _editorOptionsProvider);
+ }
+
+ public override void SetStart(TextPoint startPoint)
+ {
+ if (startPoint.TextBuffer != TextBuffer)
+ {
+ throw new ArgumentException(Strings.StartPointFromWrongBuffer);
+ }
+
+ if (startPoint.CurrentPosition > _endPoint.CurrentPosition)
+ {
+ _startPoint = _endPoint;
+ _endPoint = startPoint.Clone();
+ }
+ else
+ {
+ _startPoint = startPoint.Clone();
+ }
+ }
+
+ public override void SetEnd(TextPoint endPoint)
+ {
+ if (endPoint.TextBuffer != TextBuffer)
+ {
+ throw new ArgumentException("startPoint");
+ }
+
+ if (endPoint.CurrentPosition < _startPoint.CurrentPosition)
+ {
+ _endPoint = _startPoint;
+ _startPoint = endPoint.Clone();
+ }
+ else
+ {
+ _endPoint = endPoint.Clone();
+ }
+ }
+
+ public override void MoveTo(TextRange newRange)
+ {
+ if (newRange.TextBuffer != TextBuffer)
+ {
+ throw new ArgumentException(Strings.OtherRangeFromWrongBuffer);
+ }
+
+ _startPoint = newRange.GetStartPoint();
+ _endPoint = newRange.GetEndPoint();
+ }
+
+ protected override IEnumerator<TextPoint> GetEnumeratorInternal()
+ {
+ for (int position = _startPoint.CurrentPosition; position <= _endPoint.CurrentPosition; position++)
+ {
+ TextPoint enumeratedPoint = _startPoint.Clone();
+ enumeratedPoint.MoveTo(position);
+
+ yield return enumeratedPoint;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs
new file mode 100644
index 0000000..80715de
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs
@@ -0,0 +1,157 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Formatting;
+
+ internal sealed class DefaultTextViewPrimitive : TextView
+ {
+ private ITextView _textView;
+ private Caret _caret;
+ private Selection _selection;
+ private TextBuffer _textBuffer;
+ private IViewPrimitivesFactoryService _viewPrimitivesFactory;
+
+ internal DefaultTextViewPrimitive(ITextView textView, IViewPrimitivesFactoryService viewPrimitivesFactory, IBufferPrimitivesFactoryService bufferPrimitivesFactory)
+ {
+ _textView = textView;
+
+ _viewPrimitivesFactory = viewPrimitivesFactory;
+
+ _textBuffer = bufferPrimitivesFactory.CreateTextBuffer(textView.TextBuffer);
+
+ _caret = _viewPrimitivesFactory.CreateCaret(this);
+ _selection = _viewPrimitivesFactory.CreateSelection(this);
+ }
+
+ public override void MoveLineToTop(int lineNumber)
+ {
+ ITextSnapshotLine line = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber);
+ _textView.DisplayTextLineContainingBufferPosition(line.Start, 0.0, ViewRelativePosition.Top);
+ }
+
+ public override void MoveLineToBottom(int lineNumber)
+ {
+ ITextSnapshotLine line = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber);
+ _textView.DisplayTextLineContainingBufferPosition(line.Start, 0.0, ViewRelativePosition.Bottom);
+ }
+
+ public override void ScrollUp(int lines)
+ {
+ _textView.ViewScroller.ScrollViewportVerticallyByPixels(((double)lines) * _textView.LineHeight);
+ }
+
+ public override void ScrollDown(int lines)
+ {
+ _textView.ViewScroller.ScrollViewportVerticallyByPixels(- ((double)lines) * _textView.LineHeight);
+ }
+
+ public override void ScrollPageDown()
+ {
+ _textView.ViewScroller.ScrollViewportVerticallyByPage(ScrollDirection.Down);
+ }
+
+ public override void ScrollPageUp()
+ {
+ _textView.ViewScroller.ScrollViewportVerticallyByPage(ScrollDirection.Up);
+ }
+
+ public override bool Show(DisplayTextPoint point, HowToShow howToShow)
+ {
+ if (howToShow == HowToShow.AsIs)
+ {
+ _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(point.AdvancedTextPoint, 0), EnsureSpanVisibleOptions.MinimumScroll);
+ }
+ else if (howToShow == HowToShow.Centered)
+ {
+ _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(point.AdvancedTextPoint, 0), EnsureSpanVisibleOptions.AlwaysCenter);
+ }
+ else if (howToShow == HowToShow.OnFirstLineOfView)
+ {
+ _textView.DisplayTextLineContainingBufferPosition(point.AdvancedTextPoint, 0.0, ViewRelativePosition.Top);
+ }
+ return point.IsVisible;
+ }
+
+ public override VisibilityState Show(DisplayTextRange textRange, HowToShow howToShow)
+ {
+ if (howToShow == HowToShow.AsIs)
+ {
+ _textView.ViewScroller.EnsureSpanVisible(textRange.AdvancedTextRange, EnsureSpanVisibleOptions.MinimumScroll);
+ }
+ else if (howToShow == HowToShow.Centered)
+ {
+ _textView.ViewScroller.EnsureSpanVisible(textRange.AdvancedTextRange, EnsureSpanVisibleOptions.AlwaysCenter);
+ }
+ else if (howToShow == HowToShow.OnFirstLineOfView)
+ {
+ _textView.DisplayTextLineContainingBufferPosition(textRange.AdvancedTextRange.Start, 0.0, ViewRelativePosition.Top);
+ }
+
+ return textRange.Visibility;
+ }
+
+ public override DisplayTextPoint GetTextPoint(int position)
+ {
+ return _viewPrimitivesFactory.CreateDisplayTextPoint(this, position);
+ }
+
+ public override DisplayTextPoint GetTextPoint(TextPoint textPoint)
+ {
+ return GetTextPoint(textPoint.CurrentPosition);
+ }
+
+ public override DisplayTextPoint GetTextPoint(int line, int column)
+ {
+ ITextSnapshotLine snapshotLine = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(line);
+ return GetTextPoint(snapshotLine.Start + column);
+ }
+
+ public override DisplayTextRange GetTextRange(TextPoint startPoint, TextPoint endPoint)
+ {
+ return GetTextRange(TextBuffer.GetTextRange(startPoint, endPoint));
+ }
+
+ public override DisplayTextRange GetTextRange(TextRange textRange)
+ {
+ return _viewPrimitivesFactory.CreateDisplayTextRange(this, textRange);
+ }
+
+ public override DisplayTextRange GetTextRange(int startPosition, int endPosition)
+ {
+ return GetTextRange(TextBuffer.GetTextPoint(startPosition), TextBuffer.GetTextPoint(endPosition));
+ }
+
+ public override DisplayTextRange VisibleSpan
+ {
+ get { return GetTextRange(_textView.TextViewLines.FirstVisibleLine.Start, _textView.TextViewLines.LastVisibleLine.EndIncludingLineBreak); }
+ }
+
+ public override ITextView AdvancedTextView
+ {
+ get { return _textView; }
+ }
+
+ public override Caret Caret
+ {
+ get { return _caret; }
+ }
+
+ public override Selection Selection
+ {
+ get { return _selection; }
+ }
+
+ public override TextBuffer TextBuffer
+ {
+ get { return _textBuffer; }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs
new file mode 100644
index 0000000..09e9d25
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs
@@ -0,0 +1,72 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text.Editor;
+
+ [Export(typeof(IViewPrimitivesFactoryService))]
+ internal sealed class DefaultViewPrimitivesFactoryService : IViewPrimitivesFactoryService
+ {
+ [Import]
+ internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; }
+
+ [Import]
+ internal IBufferPrimitivesFactoryService BufferPrimitivesFactoryService { get; set; }
+
+ #region IViewPrimitivesFactoryService Members
+
+ public TextView CreateTextView(ITextView textView)
+ {
+ TextView textViewPrimitive = null;
+
+ if (!textView.Properties.TryGetProperty<TextView>(EditorPrimitiveIds.ViewPrimitiveId, out textViewPrimitive))
+ {
+ textViewPrimitive = new DefaultTextViewPrimitive(textView, this, BufferPrimitivesFactoryService);
+ textView.Properties.AddProperty(EditorPrimitiveIds.ViewPrimitiveId, textViewPrimitive);
+ }
+ return textViewPrimitive;
+ }
+
+ public DisplayTextPoint CreateDisplayTextPoint(TextView textView, int position)
+ {
+ return new DefaultDisplayTextPointPrimitive(textView, position, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView));
+ }
+
+ public DisplayTextRange CreateDisplayTextRange(TextView textView, TextRange textRange)
+ {
+ return new DefaultDisplayTextRangePrimitive(textView, textRange);
+ }
+
+ public Selection CreateSelection(TextView textView)
+ {
+ if (textView.Selection == null)
+ {
+ // The selection will add itself to the view.
+ return new DefaultSelectionPrimitive(textView, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView));
+ }
+
+ return textView.Selection;
+ }
+
+ public Caret CreateCaret(TextView textView)
+ {
+ if (textView.Caret == null)
+ {
+ // The caret will add itself to the view.
+ return new DefaultCaretPrimitive(textView, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView));
+ }
+
+ return textView.Caret;
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs
new file mode 100644
index 0000000..fd9be4f
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs
@@ -0,0 +1,39 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ [Export(typeof(IEditorPrimitivesFactoryService))]
+ internal sealed class EditorPrimitivesFactoryService : IEditorPrimitivesFactoryService
+ {
+ [Import]
+ internal IViewPrimitivesFactoryService ViewPrimitivesFactory { get; set; }
+
+ [Import]
+ internal IBufferPrimitivesFactoryService BufferPrimitivesFactory { get; set; }
+
+ #region IEditorPrimitivesFactoryService Members
+
+ public IViewPrimitives GetViewPrimitives(ITextView textView)
+ {
+ return new ViewPrimitives(textView, ViewPrimitivesFactory);
+ }
+
+ public IBufferPrimitives GetBufferPrimitives(ITextBuffer textBuffer)
+ {
+ return new BufferPrimitives(textBuffer, BufferPrimitivesFactory);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs b/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs
new file mode 100644
index 0000000..86053b8
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs
@@ -0,0 +1,81 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using System;
+ using Microsoft.VisualStudio.Text;
+
+ /// <summary>
+ /// A set of utilities to help with various calculations
+ /// in the editor primitives.
+ /// </summary>
+ internal static class PrimitivesUtilities
+ {
+ public static int GetColumnOfPoint(ITextSnapshot textSnapshot, SnapshotPoint pointPosition, SnapshotPoint startPosition, int tabSize, Func<SnapshotPoint, SnapshotPoint> getNextPosition)
+ {
+ int columnCount = 0;
+ SnapshotPoint position = startPosition;
+ while (position < pointPosition)
+ {
+ if (textSnapshot[position] == '\t')
+ {
+ // If there is a tab in the text, then the column automatically jumps
+ // to the next tab stop.
+ columnCount = ((columnCount / tabSize) + 1) * tabSize;
+ }
+ else
+ {
+ columnCount++;
+ }
+ position = getNextPosition(position);
+ }
+ return columnCount;
+ }
+
+ private static bool Edit(ITextBuffer buffer, Func<ITextEdit, bool> editAction)
+ {
+ using (ITextEdit edit = buffer.CreateEdit())
+ {
+ if (!editAction(edit))
+ return false;
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool Delete(ITextBuffer buffer, int start, int length)
+ {
+ return Edit(buffer, edit => edit.Delete(start, length));
+ }
+
+ internal static bool Insert(ITextBuffer buffer, int position, string text)
+ {
+ return Edit(buffer, edit => edit.Insert(position, text));
+ }
+
+ internal static bool Delete(ITextBuffer buffer, Span span)
+ {
+ return Edit(buffer, edit => edit.Delete(span));
+ }
+
+ internal static bool Delete(SnapshotSpan span)
+ {
+ return Edit(span.Snapshot.TextBuffer, edit => edit.Delete(span));
+ }
+
+ internal static bool Replace(ITextBuffer buffer, Span span, string replacement)
+ {
+ return Edit(buffer, edit => edit.Replace(span, replacement));
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/Strings.Designer.cs b/src/Text/Impl/EditorPrimitives/Strings.Designer.cs
new file mode 100644
index 0000000..20bf087
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/Strings.Designer.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:2.0.50727.1434
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.UI.Text.EditorPrimitives.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The other TextPoint is from a different TextBuffer than this TextPoint..
+ /// </summary>
+ internal static string OtherPointFromWrongBuffer {
+ get {
+ return ResourceManager.GetString("OtherPointFromWrongBuffer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The other TextRange is from a different TextBuffer than this TextRange..
+ /// </summary>
+ internal static string OtherRangeFromWrongBuffer {
+ get {
+ return ResourceManager.GetString("OtherRangeFromWrongBuffer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The start point&apos;s TextBuffer is different than this TextRange&apos;s TextBuffer..
+ /// </summary>
+ internal static string StartPointFromWrongBuffer {
+ get {
+ return ResourceManager.GetString("StartPointFromWrongBuffer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The TextPoint is from a TextBuffer that is not this one..
+ /// </summary>
+ internal static string TextPointFromWrongBuffer {
+ get {
+ return ResourceManager.GetString("TextPointFromWrongBuffer", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorPrimitives/Strings.resx b/src/Text/Impl/EditorPrimitives/Strings.resx
new file mode 100644
index 0000000..ec455d7
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/Strings.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="OtherPointFromWrongBuffer" xml:space="preserve">
+ <value>The other TextPoint is from a different TextBuffer than this TextPoint.</value>
+ </data>
+ <data name="OtherRangeFromWrongBuffer" xml:space="preserve">
+ <value>The other TextRange is from a different TextBuffer than this TextRange.</value>
+ </data>
+ <data name="StartPointFromWrongBuffer" xml:space="preserve">
+ <value>The start point's TextBuffer is different than this TextRange's TextBuffer.</value>
+ </data>
+ <data name="TextPointFromWrongBuffer" xml:space="preserve">
+ <value>The TextPoint is from a TextBuffer that is not this one.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs
new file mode 100644
index 0000000..9b2b1dc
--- /dev/null
+++ b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs
@@ -0,0 +1,52 @@
+//
+// 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.EditorPrimitives.Implementation
+{
+ using Microsoft.VisualStudio.Text.Editor;
+
+ internal sealed class ViewPrimitives : IViewPrimitives
+ {
+ private TextView _textView;
+ private Selection _selection;
+ private Caret _caret;
+ private TextBuffer _textBuffer;
+
+ #region IViewPrimitives Members
+
+ internal ViewPrimitives(ITextView textView, IViewPrimitivesFactoryService viewPrimitivesFactory)
+ {
+ _textView = viewPrimitivesFactory.CreateTextView(textView);
+
+ _textBuffer = _textView.TextBuffer;
+ _selection = _textView.Selection;
+ _caret = _textView.Caret;
+ }
+
+ public TextView View
+ {
+ get { return _textView; }
+ }
+
+ public Selection Selection
+ {
+ get { return _selection; }
+ }
+
+ public Caret Caret
+ {
+ get { return _caret; }
+ }
+
+ public TextBuffer Buffer
+ {
+ get { return _textBuffer; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/Navigation/DefaultTextNavigator.cs b/src/Text/Impl/Navigation/DefaultTextNavigator.cs
new file mode 100644
index 0000000..ccfc243
--- /dev/null
+++ b/src/Text/Impl/Navigation/DefaultTextNavigator.cs
@@ -0,0 +1,202 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Default Text Navigation helper.
+ /// </summary>
+ internal class DefaultTextNavigator : ITextStructureNavigator
+ {
+ #region Private Members
+
+ ITextBuffer _textBuffer;
+ IContentTypeRegistryService _contentTypeRegistry;
+
+ #endregion // Private Members
+
+ /// <summary>
+ /// Keep the constructor internal so that only the factory can instantiate our class.
+ /// </summary>
+ /// <param name="textBuffer">
+ /// The text buffer that we will navigate on.
+ /// </param>
+ /// <param name="contentTypeRegistry">
+ /// The registry for <see cref="ContentType"/>s.
+ /// </param>
+ internal DefaultTextNavigator(ITextBuffer textBuffer, IContentTypeRegistryService contentTypeRegistry)
+ {
+ // Verify
+ Debug.Assert(textBuffer != null);
+ Debug.Assert(contentTypeRegistry != null);
+
+ _textBuffer = textBuffer;
+ _contentTypeRegistry = contentTypeRegistry;
+ }
+
+ #region ITextStructureNavigator Members
+
+ /// <summary>
+ /// Get the extent of the word at the given position. IsSignificant for the extent should be set to <c>false</c> for words
+ /// consisting of whitespace, unless the whitespace is a significant part of the document. If the
+ /// returned extent is insignificant whitespace, it should include all of the adjacent whitespace,
+ /// including newline characters, spaces, and tabs.
+ /// </summary>
+ /// <param name="currentPosition">
+ /// The text position anywhere in the word whose extents are needed.
+ /// </param>
+ /// <returns>
+ /// A <see cref="TextExtent" /> describing the word. IsSignificant will be set to false for whitespace or other
+ /// insignificant 'words' that should be ignored during navigation.
+ /// </returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="currentPosition"/>is less than 0 or greater than the length of the text.</exception>
+ public TextExtent GetExtentOfWord(SnapshotPoint currentPosition)
+ {
+ if (currentPosition.Snapshot.TextBuffer != _textBuffer)
+ {
+ throw new ArgumentException("currentPosition TextBuffer does not match to the current TextBuffer");
+ }
+
+ if (currentPosition.Position >= currentPosition.Snapshot.Length - 1)
+ {
+ // End of document
+ return new TextExtent(new SnapshotSpan(currentPosition,
+ currentPosition.Snapshot.Length - currentPosition),
+ true);
+ }
+ else
+ {
+ return new TextExtent(new SnapshotSpan(currentPosition, 1), true);
+ }
+ }
+
+ /// <summary>
+ /// Get the span of the enclosing syntactic element given the currently active span.
+ /// </summary>
+ /// <param name="activeSpan">
+ /// The active span from where to get the span of the enclosing syntactic element.
+ /// </param>
+ /// <returns>
+ /// A <see cref="SnapshotSpan"/> describing the enclosing syntactic element. If the given active
+ /// span covers multiple syntactic elements, then the least common ancestor of the elements
+ /// will be returned. If it already covers the root element of the document (a.k.a the whole document),
+ /// then a <see cref="SnapshotSpan"/> of the same span will be returned.
+ /// </returns>
+ public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan)
+ {
+ if (activeSpan.IsEmpty && (activeSpan.Start != activeSpan.Snapshot.Length))
+ {
+ return new SnapshotSpan(activeSpan.Start, 1);
+ }
+ return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length);
+ }
+
+ /// <summary>
+ /// Get the span of the first child syntactic element given the currently active span.
+ /// If the active span has zero length, then the default behavior would be the same to
+ /// GetExtentOfEnclosingParent.
+ /// </summary>
+ /// <param name="activeSpan">
+ /// The active span from where to get the span of the first child syntactic element.
+ /// </param>
+ /// <returns>
+ /// A <see cref="SnapshotSpan" /> describing the first child syntactic element. If the given active
+ /// span covers multiple syntactic elements, then the span of the least common ancestor of
+ /// the elements will be returned. If it already covers the leaf level element of the document,
+ /// then a <see cref="SnapshotSpan" /> of the same span will be returned.
+ /// (such that, when the same size span returned, we will try get the extent of its enclosing
+ /// parent, which, for the third case above, will be the whole document or the whole syntactic
+ /// element depend on where the span lies).
+ /// </returns>
+ public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan)
+ {
+ if (activeSpan.IsEmpty)
+ {
+ return this.GetSpanOfEnclosing(activeSpan);
+ }
+ if (activeSpan.Length > 0 && activeSpan.Length < activeSpan.Snapshot.Length)
+ {
+ return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length);
+ }
+ return new SnapshotSpan(activeSpan.Snapshot, 0, 1);
+ }
+
+ /// <summary>
+ /// Get the span of the next sibling syntactic element given the currently active span. If the
+ /// active span has zero length, then the default behavior would be the same to
+ /// GetExtentOfEnclosingParent.
+ /// </summary>
+ /// <param name="activeSpan">
+ /// The active span from where to get the span of the next sibling syntactic element.
+ /// </param>
+ /// <returns>
+ /// A <see cref="SnapshotSpan"/> describing the next sibling syntactic element. If the given active
+ /// span covers multiple syntactic elements, then the span of the next sibling element will be
+ /// returned. If text covered by the span doesn't followed by a sibling, then the default
+ /// behavior would be the same to GetExtentOfEnclosingParent.
+ /// </returns>
+ public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan)
+ {
+ if (activeSpan.IsEmpty)
+ {
+ return this.GetSpanOfEnclosing(activeSpan);
+ }
+ if (activeSpan.End == activeSpan.Snapshot.Length)
+ {
+ return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length);
+ }
+
+ return new SnapshotSpan(activeSpan.End, 1);
+ }
+
+ /// <summary>
+ /// Get the span of the previous sibling syntactic element given the currently active span.
+ /// If the active span has zero length, then the default behavior would be the same to
+ /// GetExtentOfEnclosingParent.
+ /// </summary>
+ /// <param name="activeSpan">
+ /// The active span from where to get the span of the previous sibling syntactic element.
+ /// </param>
+ /// <returns>
+ /// A <see cref="SnapshotSpan"/> describing the next sibling syntactic element. If the given active
+ /// span covers multiple syntactic elements, then the span of the previous element will be
+ /// returned. If text covered by the span doesn't preceded by a sibling, then the default
+ /// behavior would be the same to GetExtentOfEnclosingParent.
+ /// </returns>
+ public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan)
+ {
+ if (activeSpan.IsEmpty)
+ {
+ return this.GetSpanOfEnclosing(activeSpan);
+ }
+ if (activeSpan.Start == 0)
+ {
+ return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length);
+ }
+
+ return new SnapshotSpan(activeSpan.Start - 1, 1);
+ }
+
+ /// <summary>
+ /// The content type that this navigator supports.
+ /// </summary>
+ public IContentType ContentType
+ {
+ get
+ {
+ return _contentTypeRegistry.UnknownContentType;
+ }
+ }
+
+ #endregion // ITextStructureNavigator Members
+ }
+}
diff --git a/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs
new file mode 100644
index 0000000..e59bc8d
--- /dev/null
+++ b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs
@@ -0,0 +1,98 @@
+//
+// 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.Operations.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Provides a service to help with the Text Structure Navigation.
+ /// </summary>
+ [Export(typeof(ITextStructureNavigatorSelectorService))]
+ internal sealed class TextStructureNavigatorSelectorService : ITextStructureNavigatorSelectorService
+ {
+ [Import]
+ internal IContentTypeRegistryService _contentTypeRegistryService { get; set; }
+
+ [Import]
+ internal GuardedOperations _guardedOperations { get; set; }
+
+ [ImportMany(typeof(ITextStructureNavigatorProvider))]
+ internal List<Lazy<ITextStructureNavigatorProvider, IContentTypeMetadata>> _textStructureNavigatorProviders { get; set; }
+
+ public ITextStructureNavigator GetTextStructureNavigator(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ ITextStructureNavigator navigator = null;
+
+ if (textBuffer.Properties.TryGetProperty(typeof(ITextStructureNavigator), out navigator))
+ {
+ return navigator;
+ }
+
+ navigator = CreateNavigator(textBuffer, textBuffer.ContentType);
+
+ // Cache navigator until buffer's content type changes.
+ textBuffer.Properties[typeof(ITextStructureNavigator)] = navigator;
+ textBuffer.ContentTypeChanged += OnContentTypeChanged;
+
+ return navigator;
+ }
+
+ public ITextStructureNavigator CreateTextStructureNavigator(ITextBuffer textBuffer, IContentType contentType)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ return CreateNavigator(textBuffer, contentType);
+ }
+
+ #region Private Helpers
+
+ private ITextStructureNavigator CreateNavigator(ITextBuffer textBuffer, IContentType contentType)
+ {
+ ITextStructureNavigator navigator =
+ _guardedOperations.InvokeBestMatchingFactory
+ (_textStructureNavigatorProviders,
+ contentType,
+ (provider) => (provider.CreateTextStructureNavigator(textBuffer)),
+ _contentTypeRegistryService, this);
+
+ // If we're here, and there's no navigator found, we'll create a default one
+ if (navigator == null)
+ {
+ navigator = new DefaultTextNavigator(textBuffer, _contentTypeRegistryService);
+ }
+
+ return navigator;
+ }
+
+ /// <summary>
+ /// Invalidate our cached navigator.
+ /// </summary>
+ void OnContentTypeChanged(object sender, ContentTypeChangedEventArgs e)
+ {
+ ITextBuffer buffer = e.Before.TextBuffer;
+ buffer.Properties.RemoveProperty(typeof(ITextStructureNavigator));
+ buffer.ContentTypeChanged -= OnContentTypeChanged;
+ }
+ #endregion // Private Helpers
+ }
+}
diff --git a/src/Text/Impl/Outlining/Collapsible.cs b/src/Text/Impl/Outlining/Collapsible.cs
new file mode 100644
index 0000000..5105a72
--- /dev/null
+++ b/src/Text/Impl/Outlining/Collapsible.cs
@@ -0,0 +1,105 @@
+//
+// 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.Outlining
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using System;
+
+ /// <summary>
+ /// An un-collapsed region (also the base for collapsed regions).
+ /// </summary>
+ internal class Collapsible : ICollapsible
+ {
+ public ITrackingSpan Extent { get; private set; }
+
+ public bool IsCollapsed { get; internal set; }
+
+ public object CollapsedForm
+ {
+ get { return ((Tag != null) ? Tag.CollapsedForm : null); }
+ }
+
+ public object CollapsedHintForm
+ {
+ get { return ((Tag != null) ? Tag.CollapsedHintForm : null); }
+ }
+
+ public IOutliningRegionTag Tag { get; internal set; }
+
+ public bool IsCollapsible
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ Collapsible other = obj as Collapsible;
+ if (other != null)
+ {
+ return Tag.Equals(other.Tag) &&
+ Extent.Equals(other.Extent) &&
+ IsCollapsed == other.IsCollapsed;
+ }
+
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return Tag.GetHashCode() ^ Extent.GetHashCode() ^ IsCollapsed.GetHashCode();
+ }
+
+
+ public Collapsible(ITrackingSpan underlyingSpan, IOutliningRegionTag tag)
+ {
+ Extent = underlyingSpan;
+ Tag = tag;
+
+ IsCollapsed = false;
+ }
+ }
+
+ /// <summary>
+ /// A collapsed region.
+ /// </summary>
+ internal class Collapsed : Collapsible, ICollapsed
+ {
+ public TrackingSpanNode<Collapsed> Node { get; set; }
+
+ public bool IsValid { get { return IsCollapsed; } }
+
+ public IEnumerable<ICollapsed> CollapsedChildren
+ {
+ get
+ {
+ if (!IsValid)
+ throw new InvalidOperationException("This collapsed region is no longer valid, as it has been expanded already.");
+
+ return Node.Children.Select(child => child.Item);
+ }
+ }
+
+ public void Invalidate()
+ {
+ this.IsCollapsed = false;
+ this.Node = null;
+ }
+
+ public Collapsed(ITrackingSpan extent, IOutliningRegionTag tag)
+ : base(extent, tag)
+ {
+ IsCollapsed = true;
+ }
+ }
+}
diff --git a/src/Text/Impl/Outlining/Outlining.cd b/src/Text/Impl/Outlining/Outlining.cd
new file mode 100644
index 0000000..77173b5
--- /dev/null
+++ b/src/Text/Impl/Outlining/Outlining.cd
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="Microsoft.VisualStudio.Text.Outlining.Collapsible">
+ <Position X="2.25" Y="0.75" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AwAAgAAIQAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAg=</HashCode>
+ <FileName>Out.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="Microsoft.VisualStudio.Text.Outlining.OutlineManager">
+ <Position X="5.75" Y="0.75" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AQAAgAQAAAAAAAABAIAAAAAAAAAAAAEAAAAAAAgAAAE=</HashCode>
+ <FileName>Out.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Field Name="elisionBuffer" />
+ <Field Name="subjectBuffer" />
+ </ShowAsAssociation>
+ <ShowAsCollectionAssociation>
+ <Field Name="regions" />
+ </ShowAsCollectionAssociation>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Interface Name="Microsoft.VisualStudio.Text.Projection.IElisionBuffer" Collapsed="true">
+ <Position X="10.25" Y="2" Width="1.5" />
+ <TypeIdentifier />
+ </Interface>
+ <Interface Name="Microsoft.VisualStudio.Text.ITextBuffer" Collapsed="true">
+ <Position X="10.25" Y="0.75" Width="1.5" />
+ <TypeIdentifier />
+ </Interface>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs
new file mode 100644
index 0000000..ba20323
--- /dev/null
+++ b/src/Text/Impl/Outlining/OutliningManager.cs
@@ -0,0 +1,741 @@
+//
+// 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.Outlining
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using System.Linq;
+ using System.Diagnostics;
+ using System.Threading;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Threading;
+
+ internal sealed class OutliningManager : IAccurateOutliningManager
+ {
+ private readonly ITextBuffer2 editBuffer;
+ private readonly IAccurateTagAggregator<IOutliningRegionTag> tagAggregator;
+ private bool isEnabled = true;
+ internal bool isDisposed;
+
+ // We store only the collapsed regions and generate the expanded regions on demand.
+ TrackingSpanTree<Collapsed> collapsedRegionTree;
+
+ internal OutliningManager(ITextBuffer editBuffer, ITagAggregator<IOutliningRegionTag> tagAggregator, IEditorOptions options)
+ {
+ this.editBuffer = (ITextBuffer2)editBuffer;
+ this.tagAggregator = tagAggregator as IAccurateTagAggregator<IOutliningRegionTag>;
+
+ bool keepTrackingCurrent = false;
+ if (options != null && options.IsOptionDefined("Stress Test Mode", false))
+ {
+ keepTrackingCurrent = options.GetOptionValue<bool>("Stress Test Mode");
+ }
+ collapsedRegionTree = new TrackingSpanTree<Collapsed>(editBuffer, keepTrackingCurrent);
+
+ tagAggregator.BatchedTagsChanged += OutliningRegionTagsChanged;
+ this.editBuffer.Changed += SourceTextChanged;
+ }
+
+ #region Events and event listeners
+
+ public event EventHandler<RegionsChangedEventArgs> RegionsChanged;
+ public event EventHandler<RegionsExpandedEventArgs> RegionsExpanded;
+ public event EventHandler<RegionsCollapsedEventArgs> RegionsCollapsed;
+ public event EventHandler<OutliningEnabledEventArgs> OutliningEnabledChanged;
+
+ void OutliningRegionTagsChanged(object sender, BatchedTagsChangedEventArgs e)
+ {
+ if (!isEnabled)
+ {
+ return;
+ }
+
+ // Collect the spans from the various change events
+ UpdateAfterChange(new NormalizedSnapshotSpanCollection(e.Spans.SelectMany(s => s.GetSpans(editBuffer))));
+ }
+
+ void SourceTextChanged(object sender, TextContentChangedEventArgs e)
+ {
+ if (!isEnabled)
+ {
+ return;
+ }
+
+ if (e.Changes.Count > 0)
+ {
+ UpdateAfterChange(new NormalizedSnapshotSpanCollection(e.After, e.Changes.Select(c => c.NewSpan)));
+
+ AvoidPartialLinebreaks(e);
+ }
+ }
+
+ private void AvoidPartialLinebreaks(TextContentChangedEventArgs args)
+ {
+ // The elision buffer and the view don't handle collapsed region
+ // boundaries that fall within a two-character \r\n linebreak.
+ // Currently the most common cause of such situations is regex
+ // find replace operations and this tactical fix expands affected
+ // collapsed regions in response to problematic replace operations
+ // and any other such edits.
+
+ var oldSnapshot = args.Before;
+
+ bool expandAll = false;
+
+ foreach (var change in args.Changes)
+ {
+ if (change.OldLength == 0)
+ continue;
+
+ if (change.OldPosition > 0
+ && oldSnapshot[change.OldPosition] == '\n'
+ && oldSnapshot[change.OldPosition - 1] == '\r')
+ {
+ expandAll = true;
+ break;
+ }
+
+ if (change.OldEnd > 0
+ && change.OldEnd < oldSnapshot.Length
+ && oldSnapshot[change.OldEnd] == '\n'
+ && oldSnapshot[change.OldEnd - 1] == '\r')
+ {
+ expandAll = true;
+ break;
+ }
+ }
+
+ if (expandAll)
+ {
+ this.ExpandAll(
+ new SnapshotSpan(
+ args.After,
+ Span.FromBounds(
+ args.Changes[0].NewPosition,
+ args.Changes[args.Changes.Count - 1].NewEnd)),
+ collapsed => true);
+ }
+ }
+
+ void UpdateAfterChange(NormalizedSnapshotSpanCollection changedSpans)
+ {
+ // It's possible that we've been informed of an update (via BatchedTagsChanged or otherwise) that no longer maps to the
+ // edit buffer. As a result of this, there aren't any changed spans for us to consider, so we can return immediately.
+ if (changedSpans.Count == 0)
+ return;
+
+ var currentCollapsed = GetCollapsedRegionsInternal(changedSpans, exposedRegionsOnly: false);
+
+ if (currentCollapsed.Count > 0)
+ {
+ // When getting tags, we'll try to be as minimal as possible, since
+ // this edit could be large and/or multi-part. We'll only examine
+ // the intersection of the given changed spans and the collapsed
+ // regions.
+ // NOTE: We could try to be even smarter and only use the child-most regions
+ // that we've collected, but it's a bit hard to determine which ones they
+ // are at this point (they are the nodes that have 0 children that intersect
+ // the changed spans, not just nodes that have 0 children).
+
+ var snapshot = changedSpans[0].Snapshot;
+ var spansToCheck = NormalizedSnapshotSpanCollection.Intersection(
+ changedSpans,
+ new NormalizedSnapshotSpanCollection(
+ currentCollapsed.Select(c => c.Extent.GetSpan(snapshot))));
+
+ var newCollapsibles = CollapsiblesFromTags(tagAggregator.GetTags(spansToCheck)).Keys;
+
+ IEnumerable<ICollapsed> removed;
+
+ MergeRegions(currentCollapsed, newCollapsibles, out removed);
+
+ List<ICollapsible> expandedRegions = new List<ICollapsible>();
+
+ foreach (var removedRegion in removed)
+ {
+ var expandedRegion = this.ExpandInternal(removedRegion);
+
+ expandedRegions.Add(expandedRegion);
+ }
+
+ if (expandedRegions.Count > 0)
+ {
+ // Send out the regions expanded event with the flag informing
+ // listeners that these regions are being removed.
+ var expandedEvent = RegionsExpanded;
+ if (expandedEvent != null)
+ {
+ expandedEvent(this, new RegionsExpandedEventArgs(expandedRegions, removalPending: true));
+ }
+ }
+ }
+
+ // Send out the general "outlining has changed" event
+ var handler = RegionsChanged;
+ if (handler != null)
+ {
+ handler(this, new RegionsChangedEventArgs(new SnapshotSpan(changedSpans[0].Start, changedSpans[changedSpans.Count - 1].End)));
+ }
+ }
+
+ #endregion
+
+ public ICollapsed TryCollapse(ICollapsible collapsible)
+ {
+ ICollapsed newCollapsed = CollapseInternal(collapsible);
+
+ if (newCollapsed == null)
+ return newCollapsed;
+
+ // Raise event.
+ var handler = RegionsCollapsed;
+ if (handler != null)
+ {
+ handler(this, new RegionsCollapsedEventArgs(Enumerable.Repeat(newCollapsed, 1)));
+ }
+
+ return newCollapsed;
+ }
+
+ private ICollapsed CollapseInternal(ICollapsible collapsible)
+ {
+ EnsureValid();
+
+ if (collapsible.IsCollapsed)
+ return null;
+
+ Collapsed newCollapsed = new Collapsed(collapsible.Extent, collapsible.Tag);
+
+ newCollapsed.Node = collapsedRegionTree.TryAddItem(newCollapsed, newCollapsed.Extent);
+
+ if (newCollapsed.Node == null)
+ return null;
+
+ return newCollapsed;
+ }
+
+ public ICollapsible Expand(ICollapsed collapsed)
+ {
+ ICollapsible newCollapsible = ExpandInternal(collapsed);
+
+ // Send out change event
+ var handler = RegionsExpanded;
+ if (handler != null)
+ {
+ handler(this, new RegionsExpandedEventArgs(Enumerable.Repeat(newCollapsible, 1)));
+ }
+
+ return newCollapsible;
+ }
+
+ private ICollapsible ExpandInternal(ICollapsed collapsed)
+ {
+ EnsureValid();
+
+ Collapsed internalCollapsed = collapsed as Collapsed;
+ if (internalCollapsed == null)
+ {
+ throw new ArgumentException("The given collapsed region was not created by this outlining manager.",
+ "collapsed");
+ }
+
+ if (!internalCollapsed.IsValid)
+ {
+ throw new InvalidOperationException("The collapsed region is invalid, meaning it has already been expanded.");
+ }
+
+ if (!collapsedRegionTree.RemoveItem(internalCollapsed, internalCollapsed.Extent))
+ {
+ throw new ApplicationException("Unable to remove the collapsed region from outlining manager, which means there is an internal " +
+ "consistency issue.");
+ }
+
+ // Now that we've expanded the region, invalidate the ICollapsed so it can no longer be used.
+ internalCollapsed.Invalidate();
+
+ return new Collapsible(collapsed.Extent, collapsed.Tag);
+ }
+
+ public IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match)
+ {
+ return this.InternalCollapseAll(span, match, cancel: null);
+ }
+
+ internal IEnumerable<ICollapsed> InternalCollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken? cancel)
+ {
+ if (match == null)
+ throw new ArgumentNullException("match");
+
+ EnsureValid(span);
+
+ List<ICollapsed> allCollapsed = new List<ICollapsed>();
+
+ foreach (var collapsible in this.InternalGetAllRegions(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly: false, cancel: cancel))
+ {
+ if (!collapsible.IsCollapsed && collapsible.IsCollapsible && match(collapsible))
+ {
+ var collapsed = this.CollapseInternal(collapsible);
+
+ if (collapsed != null)
+ {
+ allCollapsed.Add(collapsed);
+ }
+ }
+ }
+
+ if (allCollapsed.Count > 0)
+ {
+ // Send out change event
+ var handler = RegionsCollapsed;
+ if (handler != null)
+ {
+ handler(this, new RegionsCollapsedEventArgs(allCollapsed));
+ }
+ }
+
+ return allCollapsed;
+ }
+
+ public IEnumerable<ICollapsible> ExpandAll(SnapshotSpan span, Predicate<ICollapsed> match)
+ {
+ return ExpandAllInternal(/*removalPending = */ false, span, match);
+ }
+
+ public IEnumerable<ICollapsible> ExpandAllInternal(bool removalPending, SnapshotSpan span, Predicate<ICollapsed> match)
+ {
+ if (match == null)
+ throw new ArgumentNullException("match");
+
+ EnsureValid(span);
+
+ List<ICollapsible> allExpanded = new List<ICollapsible>();
+
+ foreach (var collapsed in this.GetCollapsedRegions(span))
+ {
+ if (match(collapsed))
+ {
+ var expanded = this.ExpandInternal(collapsed);
+
+ allExpanded.Add(expanded);
+ }
+ }
+
+ if (allExpanded.Count > 0)
+ {
+ // Send out change event
+ var handler = RegionsExpanded;
+ if (handler != null)
+ {
+ handler(this, new RegionsExpandedEventArgs(allExpanded, removalPending));
+ }
+ }
+
+ return allExpanded;
+ }
+
+ public bool Enabled
+ {
+ get
+ {
+ return this.isEnabled;
+ }
+ set
+ {
+ if (this.isEnabled != value)
+ {
+ // Expand all (if disabled)
+ ITextSnapshot snapshot = this.editBuffer.CurrentSnapshot;
+ SnapshotSpan snapshotSpan = new SnapshotSpan(snapshot, 0, snapshot.Length);
+ if (!value)
+ {
+ // Expand all regions, since we are going to remove them all
+ this.ExpandAllInternal(/*removalPending =*/ true, snapshotSpan, ((collapsed) => true));
+ }
+
+ // Update internal isEnabled flag after expanding all but before raising RegionsChanged event
+ this.isEnabled = value;
+
+ // Raise RegionsChanged event for whole buffer (Before disable event)
+ EventHandler<RegionsChangedEventArgs> regionsChanged = RegionsChanged;
+ if (regionsChanged != null && !value)
+ {
+ regionsChanged(this, new RegionsChangedEventArgs(snapshotSpan));
+ }
+
+ // Raise OutliningEnabledChanged event
+ EventHandler<OutliningEnabledEventArgs> outliningEnabledChanged = OutliningEnabledChanged;
+ if (outliningEnabledChanged != null)
+ {
+ outliningEnabledChanged(this, new OutliningEnabledEventArgs(this.isEnabled));
+ }
+
+ // Raise RegionsChanged event for whole buffer (After enable event)
+ if (regionsChanged != null && value)
+ {
+ regionsChanged(this, new RegionsChangedEventArgs(snapshotSpan));
+ }
+ }
+ }
+ }
+
+
+ #region Private helpers
+
+ private SortedList<Collapsible, object> CollapsiblesFromTags(IEnumerable<IMappingTagSpan<IOutliningRegionTag>> tagSpans)
+ {
+ ITextSnapshot current = this.editBuffer.CurrentSnapshot;
+
+ SortedList<Collapsible, object> collapsibles
+ = new SortedList<Collapsible, object>(new CollapsibleSorter(editBuffer));
+
+ foreach (var tagSpan in tagSpans)
+ {
+ var spans = tagSpan.Span.GetSpans(current);
+
+ // We only accept this tag if it hasn't been split into multiple spans and if
+ // it hasn't had pieces cut out of it from projection. Also, refuse 0-length
+ // tags, as they wouldn't be hiding anything.
+ if (spans.Count == 1 &&
+ spans[0].Length > 0 &&
+ spans[0].Length == tagSpan.Span.GetSpans(tagSpan.Span.AnchorBuffer)[0].Length)
+ {
+ ITrackingSpan trackingSpan = current.CreateTrackingSpan(spans[0], SpanTrackingMode.EdgeExclusive);
+ var collapsible = new Collapsible(trackingSpan, tagSpan.Tag);
+ if (collapsibles.ContainsKey(collapsible))
+ {
+ // TODO: Notify providers somehow.
+ // Or rewrite so that such things are legal.
+ Debug.WriteLine("IGNORING TAG " + spans[0] + " due to span conflict");
+ }
+ else
+ {
+ collapsibles.Add(collapsible, null);
+ }
+ }
+ else
+ {
+ Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because it was split or shortened by projection");
+ }
+ }
+
+ return collapsibles;
+ }
+
+ private IEnumerable<ICollapsible> MergeRegions(IEnumerable<ICollapsed> currentCollapsed, IEnumerable<ICollapsible> newCollapsibles,
+ out IEnumerable<ICollapsed> removedRegions)
+ {
+ List<ICollapsed> toRemove = new List<ICollapsed>();
+
+ List<ICollapsed> oldRegions = new List<ICollapsed>(currentCollapsed);
+ List<ICollapsible> newRegions = new List<ICollapsible>(newCollapsibles);
+
+ List<ICollapsible> merged = new List<ICollapsible>(oldRegions.Count + newRegions.Count);
+
+ int oldIndex = 0;
+ int newIndex = 0;
+
+ CollapsibleSorter sorter = new CollapsibleSorter(this.editBuffer);
+
+ while (oldIndex < oldRegions.Count || newIndex < newRegions.Count)
+ {
+ if (oldIndex < oldRegions.Count && newIndex < newRegions.Count)
+ {
+ Collapsed oldRegion = oldRegions[oldIndex] as Collapsed;
+ ICollapsible newRegion = newRegions[newIndex];
+
+ int compareVal = sorter.Compare(oldRegion, newRegion);
+
+ // Same region
+ if (compareVal == 0)
+ {
+ // might be the same region, but content could be new
+ oldRegion.Tag = newRegion.Tag;
+ merged.Add(oldRegion);
+
+ oldIndex++;
+ newIndex++;
+ }
+ // old region comes first
+ else if (compareVal < 0)
+ {
+ toRemove.Add(oldRegion);
+ oldIndex++;
+ }
+ // new region comes first
+ else if (compareVal > 0)
+ {
+ merged.Add(newRegion);
+ newIndex++;
+ }
+ }
+ else if (oldIndex < oldRegions.Count)
+ {
+ toRemove.AddRange(oldRegions.GetRange(oldIndex, oldRegions.Count - oldIndex));
+ break;
+ }
+ else if (newIndex < newRegions.Count)
+ {
+ merged.AddRange(newRegions.GetRange(newIndex, newRegions.Count - newIndex));
+ break;
+ }
+ }
+
+ removedRegions = toRemove;
+
+ return merged;
+ }
+
+ #endregion
+
+ #region Getting collapsibles
+
+ public IEnumerable<ICollapsed> GetCollapsedRegions(SnapshotSpan span)
+ {
+ return GetCollapsedRegionsInternal(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly: false);
+ }
+
+ public IEnumerable<ICollapsed> GetCollapsedRegions(SnapshotSpan span, bool exposedRegionsOnly)
+ {
+ EnsureValid(span);
+
+ return GetCollapsedRegionsInternal(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly);
+ }
+
+ public IEnumerable<ICollapsed> GetCollapsedRegions(NormalizedSnapshotSpanCollection spans)
+ {
+ return GetCollapsedRegionsInternal(spans, exposedRegionsOnly: false);
+ }
+
+ public IEnumerable<ICollapsed> GetCollapsedRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly)
+ {
+ return GetCollapsedRegionsInternal(spans, exposedRegionsOnly);
+ }
+
+ internal IList<Collapsed> GetCollapsedRegionsInternal(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly)
+ {
+ EnsureValid(spans);
+
+ // No collapsed if disabled
+ if (!isEnabled)
+ {
+ return new List<Collapsed>();
+ }
+
+ if (exposedRegionsOnly)
+ return collapsedRegionTree.FindTopLevelNodesIntersecting(spans).Select(node => node.Item).ToList();
+ else
+ return collapsedRegionTree.FindNodesIntersecting(spans).Select(node => node.Item).ToList();
+ }
+
+ public IEnumerable<ICollapsible> GetAllRegions(SnapshotSpan span)
+ {
+ return GetAllRegions(span, exposedRegionsOnly: false);
+ }
+
+ public IEnumerable<ICollapsible> GetAllRegions(SnapshotSpan span, bool exposedRegionsOnly)
+ {
+ EnsureValid(span);
+
+ return GetAllRegions(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly);
+ }
+
+ public IEnumerable<ICollapsible> GetAllRegions(NormalizedSnapshotSpanCollection spans)
+ {
+ return GetAllRegions(spans, exposedRegionsOnly: false);
+ }
+
+ public IEnumerable<ICollapsible> GetAllRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly)
+ {
+ return InternalGetAllRegions(spans, exposedRegionsOnly);
+ }
+
+ internal IEnumerable<ICollapsible> InternalGetAllRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly, CancellationToken? cancel = null)
+ {
+ EnsureValid(spans);
+
+ // No collapsibles if disabled
+ if (!isEnabled || spans.Count == 0)
+ {
+ return new List<Collapsible>();
+ }
+
+ ITextSnapshot snapshot = spans[0].Snapshot;
+
+ IList<Collapsed> currentCollapsed = GetCollapsedRegionsInternal(spans, exposedRegionsOnly);
+
+ IEnumerable<ICollapsible> newCollapsibles;
+ if (!exposedRegionsOnly || currentCollapsed.Count == 0)
+ {
+ newCollapsibles = CollapsiblesFromTags(this.InternalGetTags(spans, cancel)).Keys;
+ }
+ else
+ {
+ NormalizedSnapshotSpanCollection collapsedRegions = new NormalizedSnapshotSpanCollection(currentCollapsed.Select(c => c.Extent.GetSpan(snapshot)));
+ NormalizedSnapshotSpanCollection exposed = NormalizedSnapshotSpanCollection.Difference(spans, collapsedRegions);
+
+ // Ensure there is an empty region on each end
+ SnapshotSpan first = spans[0];
+ SnapshotSpan last = spans[spans.Count - 1];
+ NormalizedSnapshotSpanCollection ends = new NormalizedSnapshotSpanCollection(new SnapshotSpan[] { new SnapshotSpan(first.Start, 0), new SnapshotSpan(last.End, 0) });
+ exposed = NormalizedSnapshotSpanCollection.Union(exposed, ends);
+
+ newCollapsibles = CollapsiblesFromTags(this.InternalGetTags(exposed, cancel)).Keys.Where(c => IsRegionExposed(c, snapshot));
+ }
+
+ IEnumerable<ICollapsed> removed;
+
+ var merged = MergeRegions(currentCollapsed, newCollapsibles, out removed);
+
+ // NOTE: IF we have misbehaved taggers, it is possible that we'll see invalid
+ // changes here in removed regions that are currently collapsed. We can deal
+ // with this by expanding as needed, but it will cause an event to be sent out, which will
+ // likely be unexpected and cause bugs in our clients.
+
+ // There are a few ways we can deal with this:
+
+ // #1: Expand/collapse regions and event
+ foreach (var removedRegion in removed)
+ {
+ Debug.Fail("Removing a region here means a tagger has misbehaved.");
+ if (removedRegion.IsCollapsed)
+ Expand(removedRegion);
+ }
+
+ // Other options:
+ // #2: Return the new regions without doing anything special
+ // #3: Return the current collapsed + uncollapsed added regions
+
+ return merged;
+ }
+
+ private IEnumerable<IMappingTagSpan<IOutliningRegionTag>> InternalGetTags(NormalizedSnapshotSpanCollection spans, CancellationToken? cancel)
+ {
+ if (cancel.HasValue)
+ {
+ return this.tagAggregator.GetAllTags(spans, cancel.Value);
+ }
+
+ return this.tagAggregator.GetTags(spans);
+ }
+
+ bool IsRegionExposed(ICollapsible region, ITextSnapshot current)
+ {
+ var regionSpan = region.Extent.GetSpan(current);
+
+ // Filter out regions that don't have both end points exposed.
+ return !collapsedRegionTree.IsPointContainedInANode(regionSpan.Start) &&
+ !collapsedRegionTree.IsPointContainedInANode(regionSpan.End);
+ }
+
+ #endregion
+
+ #region IAccurateOutliningManager methods
+ public IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken cancel)
+ {
+ return this.InternalCollapseAll(span, match, cancel: cancel);
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ if (!this.isDisposed)
+ {
+ this.isDisposed = true;
+ this.editBuffer.Changed -= this.SourceTextChanged;
+ this.tagAggregator.BatchedTagsChanged -= this.OutliningRegionTagsChanged;
+ this.tagAggregator.Dispose();
+ }
+ }
+
+ private void EnsureValid()
+ {
+ if (this.isDisposed)
+ {
+ throw new ObjectDisposedException("OutliningManager");
+ }
+ }
+
+ private void EnsureValid(NormalizedSnapshotSpanCollection spans)
+ {
+ EnsureValid();
+
+ if (spans == null)
+ {
+ throw new ArgumentNullException("spans");
+ }
+
+ if (spans.Count == 0)
+ {
+ throw new ArgumentException("The given span collection is empty.", "spans");
+ }
+
+ if (spans[0].Snapshot.TextBuffer != this.editBuffer)
+ {
+ throw new ArgumentException("The given span collection is on an invalid buffer." +
+ "Spans must be generated against the view model's edit buffer",
+ "spans");
+ }
+ }
+
+ private void EnsureValid(SnapshotSpan span)
+ {
+ EnsureValid();
+
+ if (span.Snapshot == null)
+ {
+ throw new ArgumentException("The given span is uninitialized.");
+ }
+
+ if (span.Snapshot.TextBuffer != this.editBuffer)
+ {
+ throw new ArgumentException("The given span is on an invalid buffer." +
+ "Spans must be generated against the view model's edit buffer",
+ "span");
+ }
+ }
+
+ #endregion
+ }
+
+ #region Sorter for sorted lists of collapsibles
+ class CollapsibleSorter : IComparer<ICollapsible>
+ {
+ private ITextBuffer SourceBuffer { get; set; }
+
+ internal CollapsibleSorter(ITextBuffer sourceBuffer)
+ {
+ SourceBuffer = sourceBuffer;
+ }
+
+ public int Compare(ICollapsible x, ICollapsible y)
+ {
+ if (x == null)
+ throw new ArgumentNullException("x");
+ if (y == null)
+ throw new ArgumentNullException("y");
+
+ ITextSnapshot current = SourceBuffer.CurrentSnapshot;
+ SnapshotSpan left = x.Extent.GetSpan(current);
+ SnapshotSpan right = y.Extent.GetSpan(current);
+
+ // The "first" collapsible should come first
+ if (left.Start != right.Start)
+ return left.Start.CompareTo(right.Start);
+ // The largest collapsible should come first
+ else
+ return -left.Length.CompareTo(right.Length);
+ }
+ }
+ #endregion
+}
diff --git a/src/Text/Impl/Outlining/OutliningManagerService.cs b/src/Text/Impl/Outlining/OutliningManagerService.cs
new file mode 100644
index 0000000..5e8a795
--- /dev/null
+++ b/src/Text/Impl/Outlining/OutliningManagerService.cs
@@ -0,0 +1,45 @@
+//
+// 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.Outlining
+{
+ using System;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.Win32;
+
+ [Export(typeof(IOutliningManagerService))]
+ internal class OutliningManagerService : IOutliningManagerService
+ {
+ [Import]
+ internal IBufferTagAggregatorFactoryService TagAggregatorFactory { get; set; }
+
+ [Import]
+ internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; }
+
+ // While these are IDisposable, they are kept for the life of the textView, meaning that callers shouldn't actually
+ // dispose of them.
+ public IOutliningManager GetOutliningManager(ITextView textView)
+ {
+ if (textView == null)
+ throw new ArgumentNullException("textView");
+
+ if (!textView.Roles.Contains(PredefinedTextViewRoles.Structured))
+ return null;
+
+ return textView.Properties.GetOrCreateSingletonProperty(delegate
+ {
+ var tagAggregator = TagAggregatorFactory.CreateTagAggregator<IOutliningRegionTag>(textView.TextBuffer);
+ var manager = new OutliningManager(textView.TextBuffer, tagAggregator, EditorOptionsFactoryService.GlobalOptions);
+ textView.Closed += delegate { manager.Dispose(); };
+ return manager;
+ });
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/AutoEnclose.cs b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs
new file mode 100644
index 0000000..42af7d4
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs
@@ -0,0 +1,29 @@
+//
+// 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.
+//
+using System;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ internal delegate void AutoEncloseDelegate();
+
+ internal class AutoEnclose : IDisposable
+ {
+ private AutoEncloseDelegate end;
+
+ public AutoEnclose(AutoEncloseDelegate end)
+ {
+ this.end = end;
+ }
+
+ public void Dispose()
+ {
+ if (end != null) end();
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs
new file mode 100644
index 0000000..b39446d
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs
@@ -0,0 +1,38 @@
+//
+// 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.
+//
+using System;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ /// <summary>
+ /// This class is to make it easy to catch new undo/redo operations while a delegated primitive
+ /// is in progress--it is called from DelegatedUndoPrimitive.Undo and .Redo with the IDispose
+ /// using pattern to set up the history to send operations our way.
+ /// </summary>
+ internal class CatchOperationsFromHistoryForDelegatedPrimitive : IDisposable
+ {
+ UndoHistoryImpl history;
+ DelegatedUndoPrimitiveImpl primitive;
+
+ public CatchOperationsFromHistoryForDelegatedPrimitive(UndoHistoryImpl history, DelegatedUndoPrimitiveImpl primitive, DelegatedUndoPrimitiveState state)
+ {
+ this.history = history;
+ this.primitive = primitive;
+
+ primitive.State = state;
+ history.ForwardToUndoOperation(primitive);
+ }
+
+ public void Dispose()
+ {
+ history.EndForwardToUndoOperation(primitive);
+ primitive.State = DelegatedUndoPrimitiveState.Inactive;
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs
new file mode 100644
index 0000000..87c83f9
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs
@@ -0,0 +1,128 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ /// <summary>
+ /// This is the implementation of a primitive to support inverse operations, where the user does not supply their own
+ /// primitives. Rather, the user calls "AddUndo" on the history and we build the primitive for them.
+ /// </summary>
+ internal class DelegatedUndoPrimitiveImpl : ITextUndoPrimitive
+ {
+ private Stack<UndoableOperationCurried> redoOperations;
+ private Stack<UndoableOperationCurried> undoOperations;
+ private UndoTransactionImpl parent;
+ private readonly UndoHistoryImpl history;
+ private DelegatedUndoPrimitiveState state;
+
+ public DelegatedUndoPrimitiveState State
+ {
+ get { return state; }
+ set { state = value; }
+ }
+
+ public DelegatedUndoPrimitiveImpl(UndoHistoryImpl history, UndoTransactionImpl parent, UndoableOperationCurried operationCurried)
+ {
+ redoOperations = new Stack<UndoableOperationCurried>();
+ undoOperations = new Stack<UndoableOperationCurried>();
+
+ this.parent = parent;
+ this.history = history;
+ this.state = DelegatedUndoPrimitiveState.Inactive;
+
+ undoOperations.Push(operationCurried);
+ }
+
+ public bool CanRedo
+ {
+ get { return redoOperations.Count > 0; }
+ }
+
+ public bool CanUndo
+ {
+ get { return undoOperations.Count > 0; }
+ }
+
+
+ /// <summary>
+ /// Here, we undo everything in the list of undo operations, and then clear the list. While this is happening, the
+ /// History will collect new operations for the redo list and pass them on to us.
+ /// </summary>
+ public void Undo()
+ {
+ using (new CatchOperationsFromHistoryForDelegatedPrimitive(history, this, DelegatedUndoPrimitiveState.Undoing))
+ {
+ while (undoOperations.Count > 0)
+ {
+ undoOperations.Pop()();
+ }
+ }
+ }
+
+ /// <summary>
+ /// This is only called for "Redo," not for the original "Do." The action is to redo everything in the list of
+ /// redo operations, and then clear the list. While this is happening, the History will collect new operations
+ /// for the undo list and pass them on to us.
+ /// </summary>
+ public void Do()
+ {
+ using (new CatchOperationsFromHistoryForDelegatedPrimitive(history, this, DelegatedUndoPrimitiveState.Redoing))
+ {
+ while (redoOperations.Count > 0)
+ {
+ redoOperations.Pop()();
+ }
+ }
+ }
+
+ public ITextUndoTransaction Parent
+ {
+ get { return this.parent; }
+ set { this.parent = value as UndoTransactionImpl; }
+ }
+
+ /// <summary>
+ /// This is called by the UndoHistory implementation when we are mid-undo/mid-redo and
+ /// the history receives a new UndoableOperation. The action is then to add that operation
+ /// to the inverse list.
+ /// </summary>
+ /// <param name="operation"></param>
+ public void AddOperation(UndoableOperationCurried operation)
+ {
+ if (this.state == DelegatedUndoPrimitiveState.Redoing)
+ {
+ undoOperations.Push(operation);
+ }
+ else if (this.state == DelegatedUndoPrimitiveState.Undoing)
+ {
+ redoOperations.Push(operation);
+ }
+ else
+ {
+ throw new InvalidOperationException("Strings.DelegatedUndoPrimitiveStateDoesNotAllowAdd");
+ }
+ }
+
+ public bool MergeWithPreviousOnly
+ {
+ get { return true; }
+ }
+
+ public bool CanMerge(ITextUndoPrimitive primitive)
+ {
+ return false;
+ }
+
+ public ITextUndoPrimitive Merge(ITextUndoPrimitive primitive)
+ {
+ throw new InvalidOperationException("Strings.DelegatedUndoPrimitiveCannotMerge");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs
new file mode 100644
index 0000000..4493465
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs
@@ -0,0 +1,32 @@
+//
+// 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.Operations.Standalone
+{
+ /// <summary>
+ /// These are the three states for the DelegatedUndoPrimitives. If Redoing or Undoing, a Redo or undo is in progress. In the
+ /// inactive case, it is illegal to send new operations to the primitive.
+ /// </summary>
+ internal enum DelegatedUndoPrimitiveState
+ {
+ /// <summary>
+ /// No redo or undo is in progress, and it is illegal to send new operations to the primitive.
+ /// </summary>
+ Inactive,
+
+ /// <summary>
+ /// A redo is in progress. New operations go into the undo list.
+ /// </summary>
+ Redoing,
+
+ /// <summary>
+ /// An undo is in progress. New operations go into the redo list.
+ /// </summary>
+ Undoing
+ }
+
+}
diff --git a/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs b/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs
new file mode 100644
index 0000000..1f897dc
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs
@@ -0,0 +1,60 @@
+//
+// 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.
+//
+using System;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ /// <summary>
+ /// Represents an empty <see cref="IMergeTextUndoTransactionPolicy"/> implementation, which disallows merging between transactions.
+ /// </summary>
+ public sealed class NullMergeUndoTransactionPolicy : IMergeTextUndoTransactionPolicy
+ {
+ #region Private Fields
+
+ private static NullMergeUndoTransactionPolicy instance;
+
+ #endregion
+
+ #region Private Constructor
+
+ private NullMergeUndoTransactionPolicy() { }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the <see cref="NullMergeUndoTransactionPolicy"/> object.
+ /// </summary>
+ public static IMergeTextUndoTransactionPolicy Instance
+ {
+ get
+ {
+ if (NullMergeUndoTransactionPolicy.instance == null)
+ {
+ NullMergeUndoTransactionPolicy.instance = new NullMergeUndoTransactionPolicy();
+ }
+
+ return instance;
+ }
+ }
+
+ public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other)
+ {
+ return false;
+ }
+
+ public bool CanMerge(ITextUndoTransaction newerTransaction, ITextUndoTransaction olderTransaction)
+ {
+ return false;
+ }
+
+ public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction)
+ {
+ throw new InvalidOperationException("Strings.NullMergePolicyCannotMerge");
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs
new file mode 100644
index 0000000..9f5bac5
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs
@@ -0,0 +1,546 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Collections.ObjectModel;
+using System.ComponentModel.Composition;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ internal class UndoHistoryImpl : ITextUndoHistory
+ {
+ public event EventHandler<TextUndoRedoEventArgs> UndoRedoHappened;
+ public event EventHandler<TextUndoTransactionCompletedEventArgs> UndoTransactionCompleted;
+
+ #region Private Fields
+
+ private UndoTransactionImpl currentTransaction;
+ private Stack<ITextUndoTransaction> undoStack;
+ private Stack<ITextUndoTransaction> redoStack;
+ private DelegatedUndoPrimitiveImpl activeUndoOperationPrimitive;
+ private TextUndoHistoryState state;
+ private PropertyCollection properties;
+
+ #endregion
+
+ internal UndoHistoryRegistryImpl UndoHistoryRegistry;
+
+ public UndoHistoryImpl(UndoHistoryRegistryImpl undoHistoryRegistry)
+ {
+ this.currentTransaction = null;
+ this.UndoHistoryRegistry = undoHistoryRegistry;
+ this.undoStack = new Stack<ITextUndoTransaction>();
+ this.redoStack = new Stack<ITextUndoTransaction>();
+ this.activeUndoOperationPrimitive = null;
+ this.state = TextUndoHistoryState.Idle;
+ }
+
+ /// <summary>
+ /// The full undo stack for this history. Does not include any currently opened or redo transactions.
+ /// </summary>
+ public IEnumerable<ITextUndoTransaction> UndoStack
+ {
+ get { return this.undoStack; }
+ }
+
+ /// <summary>
+ /// The full redo stack for this history. Does not include any currently opened or undo transactions.
+ /// </summary>
+ public IEnumerable<ITextUndoTransaction> RedoStack
+ {
+ get { return this.redoStack; }
+ }
+
+ /// <summary>
+ /// It returns most recently pushed (topmost) item of the <see cref="ITextUndoHistory.UndoStack"/> or if the stack is
+ /// empty it returns null.
+ /// </summary>
+ public ITextUndoTransaction LastUndoTransaction
+ {
+ get
+ {
+ if (this.undoStack.Count != 0)
+ {
+ return this.undoStack.Peek();
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// It returns most recently pushed (topmost) item of the <see cref="ITextUndoHistory.RedoStack"/> or if the stack is
+ /// empty it returns null.
+ /// </summary>
+ public ITextUndoTransaction LastRedoTransaction
+ {
+ get
+ {
+ if (this.redoStack.Count != 0)
+ {
+ return this.redoStack.Peek();
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Whether a single undo is permissible (corresponds to the most recent visible undo UndoTransaction's CanUndo).
+ /// </summary>
+ /// <remarks>
+ /// If there are hidden transactions on top of the visible transaction, this property returns true only they are
+ /// undoable as well.
+ /// </remarks>
+ public bool CanUndo
+ {
+ get
+ {
+ if (this.undoStack.Count > 0)
+ {
+ return this.undoStack.Peek().CanUndo;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Whether a single redo is permissible (corresponds to the most recent visible redo UndoTransaction's CanRedo).
+ /// </summary>
+ /// <remarks>
+ /// If there are hidden transactions on top of the visible transaction, this property returns true only they are
+ /// redoable as well.
+ /// </remarks>
+ public bool CanRedo
+ {
+ get
+ {
+ if (this.redoStack.Count > 0)
+ {
+ return this.redoStack.Peek().CanRedo;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// The most recent visible undo UndoTransactions's Description.
+ /// </summary>
+ public string UndoDescription
+ {
+ get
+ {
+ if (this.undoStack.Count > 0)
+ {
+ return this.undoStack.Peek().Description;
+ }
+ else
+ {
+ return "Strings.HistoryCantUndo";
+ }
+ }
+ }
+
+ /// <summary>
+ /// The most recent visible redo UndoTransaction's Description.
+ /// </summary>
+ public string RedoDescription
+ {
+ get
+ {
+ if (this.undoStack.Count > 0)
+ {
+ return this.redoStack.Peek().Description;
+ }
+ else
+ {
+ return "Strings.HistoryCantRedo";
+ }
+ }
+ }
+
+ /// <summary>
+ /// The current UndoTransaction in progress.
+ /// </summary>
+ public ITextUndoTransaction CurrentTransaction
+ {
+ get { return this.currentTransaction; }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ public TextUndoHistoryState State
+ {
+ get { return this.state; }
+ }
+
+ /// <summary>
+ /// Creates a new transaction, nests it in the previously current transaction, and marks it current.
+ /// If there is a redo stack, it gets cleared.
+ /// UNDONE: should the redo-clearing happen now or when the new transaction is committed?
+ /// </summary>
+ /// <param name="description">A string description for the transaction.</param>
+ /// <param name="isHidden">The new transaction.</param>
+ /// <returns></returns>
+ public ITextUndoTransaction CreateTransaction(string description)
+ {
+ if (String.IsNullOrEmpty(description))
+ {
+ throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "CreateTransaction", "description"));
+ }
+
+ // If there is a pending transaction that has already been completed, we should not be permitted
+ // to open a new transaction, since it cannot later be added to its parent.
+ if ((this.currentTransaction != null) && (this.currentTransaction.State != UndoTransactionState.Open))
+ {
+ throw new InvalidOperationException("Strings.CannotCreateTransactionWhenCurrentTransactionNotOpen");
+ }
+
+ // new transactions that are visible should clear the redo stack.
+ if (this.currentTransaction == null)
+ {
+ foreach (UndoTransactionImpl redoTransaction in this.redoStack)
+ {
+ redoTransaction.Invalidate();
+ }
+
+ this.redoStack.Clear();
+ }
+
+ UndoTransactionImpl newTransaction = new UndoTransactionImpl(this, this.currentTransaction, description);
+
+ this.currentTransaction = newTransaction;
+
+ return this.currentTransaction;
+ }
+
+ /// <summary>
+ /// Performs requested amount of undo operation and places the transactions on the redo stack.
+ /// UNDONE: What if there is a currently opened transaction?
+ /// </summary>
+ /// <param name="count">The number of undo operations to perform. At the end of the operation, requested number of visible
+ /// transactions are undone. Hence actual number of transactions undone might be more than this number if there are some
+ /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones.
+ /// </param>
+ /// <remarks>
+ /// After the last visible transaction is undone, hidden transactions left on top the stack are undone as well until a
+ /// visible or linked transaction is encountered or stack is emptied totally.
+ /// </remarks>
+ public void Undo(int count)
+ {
+ if (count <= 0)
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Undo", count), "count");
+ }
+
+ if (!IsThereEnoughVisibleTransactions(this.undoStack, count))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "undo", count));
+ }
+
+ TextUndoHistoryState originalState = this.state;
+ this.state = TextUndoHistoryState.Undoing;
+ using (new AutoEnclose(delegate { this.state = originalState; }))
+ {
+ while (count > 0)
+ {
+ if (!this.undoStack.Peek().CanUndo)
+ {
+ throw new InvalidOperationException("Strings.CannotUndoRequestedPrimitiveFromHistoryUndo");
+ }
+
+ ITextUndoTransaction ut = this.undoStack.Pop();
+ ut.Undo();
+ this.redoStack.Push(ut);
+
+ RaiseUndoRedoHappened(this.state, ut);
+
+ --count;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs an undo operation and places the primitives on the redo stack, up until (and
+ /// including) the transaction indicated. This is called by the linked undo transaction that
+ /// is aware of the linking relationship between transactions, and it does not call back into
+ /// the transactions' public Undo().
+ /// </summary>
+ /// <param name="transaction"></param>
+ public void UndoInIsolation(UndoTransactionImpl transaction)
+ {
+ TextUndoHistoryState originalState = this.state;
+ this.state = TextUndoHistoryState.Undoing;
+ using (new AutoEnclose(delegate { this.state = originalState; }))
+ {
+
+ if (this.undoStack.Contains(transaction))
+ {
+ UndoTransactionImpl undone = null;
+ while (undone != transaction)
+ {
+ UndoTransactionImpl ut = this.undoStack.Pop() as UndoTransactionImpl;
+ ut.Undo();
+ this.redoStack.Push(ut);
+
+ RaiseUndoRedoHappened(this.state, ut);
+
+ undone = ut;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs requested amount of redo operation and places the transactions on the undo stack.
+ /// UNDONE: What if there is a currently opened transaction?
+ /// </summary>
+ /// <param name="count">The number of redo operations to perform. At the end of the operation, requested number of visible
+ /// transactions are redone. Hence actual number of transactions redone might be more than this number if there are some
+ /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones.
+ /// </param>
+ /// <remarks>
+ /// After the last visible transaction is redone, hidden transactions left on top the stack are redone as well until a
+ /// visible or linked transaction is encountered or stack is emptied totally.
+ /// </remarks>
+ public void Redo(int count)
+ {
+ if (count <= 0)
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Redo", count), "count");
+ }
+
+ if (!IsThereEnoughVisibleTransactions(this.redoStack, count))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "redo", count));
+ }
+
+ TextUndoHistoryState originalState = this.state;
+ this.state = TextUndoHistoryState.Redoing;
+ using (new AutoEnclose(delegate { this.state = originalState; }))
+ {
+ while (count > 0)
+ {
+ if (!this.redoStack.Peek().CanRedo)
+ {
+ throw new InvalidOperationException("Strings.CannotRedoRequestedPrimitiveFromHistoryRedo");
+ }
+ ITextUndoTransaction ut = this.redoStack.Pop();
+ ut.Do();
+ this.undoStack.Push(ut);
+
+ RaiseUndoRedoHappened(this.state, ut);
+
+ --count;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs a redo operation and places the primitives on the redo stack, up until (and
+ /// including) the transaction indicated. This is called by the linked undo transaction that
+ /// is aware of the linking relationship between transactions, and it does not call back into
+ /// the transactions' public Redo().
+ /// </summary>
+ /// <param name="transaction"></param>
+ public void RedoInIsolation(UndoTransactionImpl transaction)
+ {
+ TextUndoHistoryState originalState = this.state;
+ this.state = TextUndoHistoryState.Redoing;
+ using (new AutoEnclose(delegate { this.state = originalState; }))
+ {
+ if (this.redoStack.Contains(transaction))
+ {
+ UndoTransactionImpl redone = null;
+ while (redone != transaction)
+ {
+ UndoTransactionImpl ut = this.redoStack.Pop() as UndoTransactionImpl;
+ ut.Do();
+ this.undoStack.Push(ut);
+
+ RaiseUndoRedoHappened(this.state, ut);
+
+ redone = ut;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// This method is called from the DelegatedUndoPrimitive just as it starts a do or undo, so that this
+ /// history knows to forward any new UndoableOperations to the primitive. This and its pair EndForward... only manage
+ /// the state of the activeUndoOperationPrimitive.
+ /// </summary>
+ /// <param name="primitive">The delegated primitive to be marked active</param>
+ public void ForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive)
+ {
+ if (this.activeUndoOperationPrimitive != null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ this.activeUndoOperationPrimitive = primitive;
+ }
+
+ /// <summary>
+ /// This method ends the lifetime of the activeUndoOperationPrimitive and should be called after ForwardToUndoOperation.
+ /// </summary>
+ /// <param name="primitive">The previously active delegated primitive--used for sanity check.</param>
+ public void EndForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive)
+ {
+ if (this.activeUndoOperationPrimitive != primitive)
+ {
+ throw new InvalidOperationException();
+ }
+
+ this.activeUndoOperationPrimitive = null;
+ }
+
+ /// <summary>
+ /// This is how the transactions alert their containing history that they have finished
+ /// (likely from the Dispose() method).
+ /// </summary>
+ /// <param name="transaction">This is the transaction that's finishing. It should match the history's current transaction.
+ /// If it does not match, then the current transaction will be discarded and an exception will be thrown.</param>
+ public void EndTransaction(ITextUndoTransaction transaction)
+ {
+ if (this.currentTransaction != transaction)
+ {
+ this.currentTransaction = null;
+ throw new InvalidOperationException("Strings.EndTransactionOutOfOrder");
+ }
+
+ // only add completed transactions to their parents (or the stack)
+ if (this.currentTransaction.State == UndoTransactionState.Completed)
+ {
+ if (this.currentTransaction.Parent == null) // stack bottomed out!
+ {
+ MergeOrPushToUndoStack(this.currentTransaction);
+ }
+ }
+ this.currentTransaction = this.currentTransaction.Parent as UndoTransactionImpl;
+ }
+
+ /// <summary>
+ /// This does two different things, depending on the MergeUndoTransactionPolicys in question.
+ /// It either simply pushes the current transaction to the undo stack, OR it merges it with
+ /// the most recent item in the stack.
+ /// </summary>
+ private void MergeOrPushToUndoStack(UndoTransactionImpl transaction)
+ {
+ ITextUndoTransaction transactionAdded;
+ TextUndoTransactionCompletionResult transactionResult;
+
+ UndoTransactionImpl utPrevious = this.undoStack.Count > 0 ? this.undoStack.Peek() as UndoTransactionImpl : null;
+ if (utPrevious != null && ProceedWithMerge(transaction, utPrevious))
+ {
+ // Temporarily make utPrevious non-read-only, during merge.
+ utPrevious.IsReadOnly = false;
+ try
+ {
+ transaction.MergePolicy.PerformTransactionMerge(utPrevious, transaction);
+ }
+ finally
+ {
+ utPrevious.IsReadOnly = true;
+ }
+
+ // utPrevious is already on the undo stack, so we don't need to add it; but report
+ // it as the added transaction in the UndoTransactionCompleted event.
+ transactionAdded = utPrevious;
+ transactionResult = TextUndoTransactionCompletionResult.TransactionMerged;
+ }
+ else
+ {
+ this.undoStack.Push(transaction);
+
+ transactionAdded = transaction;
+ transactionResult = TextUndoTransactionCompletionResult.TransactionAdded;
+ }
+ RaiseUndoTransactionCompleted(transactionAdded, transactionResult);
+ }
+
+ public bool ValidTransactionForMarkers(ITextUndoTransaction transaction)
+ {
+ return transaction == null // you can put a marker on the null transaction
+ || this.currentTransaction == transaction // you can put a marker on the currently active transaction
+ || (transaction.History == this && !(transaction.State == UndoTransactionState.Invalid));
+ // and you can put a marker on any transaction in this history.
+ }
+
+ public static bool IsThereEnoughVisibleTransactions(Stack<ITextUndoTransaction> stack, int visibleCount)
+ {
+ if (visibleCount <= 0)
+ {
+ return true;
+ }
+
+ foreach (ITextUndoTransaction transaction in stack)
+ {
+ visibleCount--;
+
+ if (visibleCount <= 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool ProceedWithMerge(UndoTransactionImpl transaction1, UndoTransactionImpl transaction2)
+ {
+ UndoHistoryRegistryImpl registry = UndoHistoryRegistry;
+
+ return transaction1.MergePolicy != null
+ && transaction2.MergePolicy != null
+ && transaction1.MergePolicy.TestCompatiblePolicy(transaction2.MergePolicy)
+ && transaction1.MergePolicy.CanMerge(transaction1, transaction2);
+ }
+
+ private void RaiseUndoRedoHappened(TextUndoHistoryState state, ITextUndoTransaction transaction)
+ {
+ EventHandler<TextUndoRedoEventArgs> undoRedoHappened = UndoRedoHappened;
+ if (undoRedoHappened != null)
+ {
+ undoRedoHappened(this, new TextUndoRedoEventArgs(state, transaction));
+ }
+ }
+
+ private void RaiseUndoTransactionCompleted(ITextUndoTransaction transaction, TextUndoTransactionCompletionResult result)
+ {
+ EventHandler<TextUndoTransactionCompletedEventArgs> undoTransactionAdded = UndoTransactionCompleted;
+ if (undoTransactionAdded != null)
+ {
+ undoTransactionAdded(this, new TextUndoTransactionCompletedEventArgs(transaction, result));
+ }
+ }
+
+ public PropertyCollection Properties
+ {
+ get
+ {
+ if (this.properties == null)
+ {
+ this.properties = new PropertyCollection();
+ }
+ return this.properties;
+ }
+ }
+ }
+}
+
diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs
new file mode 100644
index 0000000..2e4742d
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs
@@ -0,0 +1,273 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.ComponentModel.Composition;
+using System.ComponentModel;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ [Export(typeof(ITextUndoHistoryRegistry))]
+ [Export(typeof(UndoHistoryRegistryImpl))]
+ internal class UndoHistoryRegistryImpl : ITextUndoHistoryRegistry
+ {
+ #region Private Fields
+ private Dictionary<ITextUndoHistory, int> histories;
+ private Dictionary<WeakReferenceForDictionaryKey, ITextUndoHistory> weakContextMapping;
+ private Dictionary<object, ITextUndoHistory> strongContextMapping;
+ #endregion // Private Fields
+
+ public UndoHistoryRegistryImpl()
+ {
+ // set up the list of histories
+ histories = new Dictionary<ITextUndoHistory, int>();
+
+ // set up the mappings from contexts to histories
+ weakContextMapping = new Dictionary<WeakReferenceForDictionaryKey, ITextUndoHistory>();
+ strongContextMapping = new Dictionary<object, ITextUndoHistory>();
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ public IEnumerable<ITextUndoHistory> Histories
+ {
+ get { return histories.Keys; }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public ITextUndoHistory RegisterHistory(object context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context"));
+ }
+
+ return RegisterHistory(context, false);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="keepAlive"></param>
+ /// <returns></returns>
+ public ITextUndoHistory RegisterHistory(object context, bool keepAlive)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context"));
+ }
+
+ ITextUndoHistory result;
+
+ if (strongContextMapping.ContainsKey(context))
+ {
+ result = strongContextMapping[context];
+
+ if (!keepAlive)
+ {
+ strongContextMapping.Remove(context);
+ weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), result);
+ }
+ }
+ else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context)))
+ {
+ result = weakContextMapping[new WeakReferenceForDictionaryKey(context)];
+
+ if (keepAlive)
+ {
+ weakContextMapping.Remove(new WeakReferenceForDictionaryKey(context));
+ strongContextMapping.Add(context, result);
+ }
+ }
+ else
+ {
+ result = new UndoHistoryImpl(this);
+ histories.Add(result, 1);
+
+ if (keepAlive)
+ {
+ strongContextMapping.Add(context, result);
+ }
+ else
+ {
+ weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), result);
+ }
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public ITextUndoHistory GetHistory(object context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "GetHistory", "context"));
+ }
+
+ ITextUndoHistory result;
+
+ if (strongContextMapping.ContainsKey(context))
+ {
+ result = strongContextMapping[context];
+ }
+ else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context)))
+ {
+ result = weakContextMapping[new WeakReferenceForDictionaryKey(context)];
+ }
+ else
+ {
+ throw new InvalidOperationException("Strings.GetHistoryCannotFindContextInRegistry");
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="history"></param>
+ /// <returns></returns>
+ public bool TryGetHistory(object context, out ITextUndoHistory history)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "TryGetHistory", "context"));
+ }
+
+ ITextUndoHistory result = null;
+
+ if (strongContextMapping.ContainsKey(context))
+ {
+ result = strongContextMapping[context];
+ }
+ else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context)))
+ {
+ result = weakContextMapping[new WeakReferenceForDictionaryKey(context)];
+ }
+
+ history = result;
+ return (result != null);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="history"></param>
+ public void AttachHistory(object context, ITextUndoHistory history)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context"));
+ }
+
+ if (history == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history"));
+ }
+
+ AttachHistory(context, history, false);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="history"></param>
+ /// <param name="keepAlive"></param>
+ public void AttachHistory(object context, ITextUndoHistory history, bool keepAlive)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context"));
+ }
+
+ if (history == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history"));
+ }
+
+ if (strongContextMapping.ContainsKey(context) || weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context)))
+ {
+ throw new InvalidOperationException("Strings.AttachHistoryAlreadyContainsContextInRegistry");
+ }
+
+ if (!histories.ContainsKey(history))
+ {
+ histories.Add(history, 1);
+ }
+ else
+ {
+ ++histories[history];
+ }
+
+ if (keepAlive)
+ {
+ strongContextMapping.Add(context, history);
+ }
+ else
+ {
+ weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), history);
+ }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="history"></param>
+ public void RemoveHistory(ITextUndoHistory history)
+ {
+ if (history == null)
+ {
+ throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RemoveHistory", "history"));
+ }
+
+ if (!histories.ContainsKey(history))
+ {
+ return;
+ }
+
+ histories.Remove(history);
+
+ List<object> strongToRemove = new List<object>();
+ foreach (object o in strongContextMapping.Keys)
+ {
+ if (Object.ReferenceEquals(strongContextMapping[o], history))
+ {
+ strongToRemove.Add(o);
+ }
+ }
+ strongToRemove.ForEach(delegate(object o) { strongContextMapping.Remove(o); });
+
+ List<WeakReferenceForDictionaryKey> weakToRemove = new List<WeakReferenceForDictionaryKey>();
+ foreach (WeakReferenceForDictionaryKey o in weakContextMapping.Keys)
+ {
+ if (Object.ReferenceEquals(weakContextMapping[o], history))
+ {
+ weakToRemove.Add(o);
+ }
+ }
+ weakToRemove.ForEach(delegate(WeakReferenceForDictionaryKey o) { weakContextMapping.Remove(o); });
+
+ return;
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs
new file mode 100644
index 0000000..aae1d06
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs
@@ -0,0 +1,397 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Collections.ObjectModel;
+using System.ComponentModel.Composition;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ internal class UndoTransactionImpl : ITextUndoTransaction
+ {
+ #region Private Fields
+
+ private readonly UndoHistoryImpl history;
+ private readonly UndoTransactionImpl parent;
+
+ private string description;
+ private UndoTransactionState state;
+ private List<ITextUndoPrimitive> primitives;
+ private IMergeTextUndoTransactionPolicy mergePolicy;
+
+ #endregion
+
+ public UndoTransactionImpl(ITextUndoHistory history, ITextUndoTransaction parent, string description)
+ {
+ if (history == null)
+ {
+ throw new ArgumentNullException("history", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "history"));
+ }
+
+ if (String.IsNullOrEmpty(description))
+ {
+ throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "description"));
+ }
+
+ this.history = history as UndoHistoryImpl;
+
+ if (this.history == null)
+ {
+ throw new ArgumentException("Strings.InvalidHistoryInTransaction");
+ }
+
+ this.parent = parent as UndoTransactionImpl;
+
+ if (this.parent == null && parent != null)
+ {
+ throw new ArgumentException("Strings.InvalidParentInTransaction");
+ }
+
+ this.description = description;
+
+ this.state = UndoTransactionState.Open;
+ this.primitives = new List<ITextUndoPrimitive>();
+ this.mergePolicy = NullMergeUndoTransactionPolicy.Instance;
+ this.IsReadOnly = true;
+ }
+
+ /// <summary>
+ /// This is how you turn transaction into "Invalid" state. Use it to indicate that this transaction is retired forever,
+ /// such as when clearing transactions from the redo stack.
+ /// </summary>
+ internal void Invalidate()
+ {
+ this.state = UndoTransactionState.Invalid;
+ }
+
+ internal bool IsInvalid
+ {
+ get { return this.state == UndoTransactionState.Invalid; }
+ }
+
+ /// <summary>
+ /// Used by UndoHistoryImpl.cs to allow UndoPrimitives to be modified during merging.
+ /// </summary>
+ internal bool IsReadOnly { get; set; }
+
+ /// <summary>
+ /// Description is the [localized] string that describes the transaction to a user.
+ /// </summary>
+ public string Description
+ {
+ get { return this.description; }
+ set { this.description = value; }
+ }
+
+ /// <summary>
+ /// State is the UndoTransactionState for the UndoTransaction, as described in that type.
+ /// </summary>
+ public UndoTransactionState State
+ {
+ get { return this.state; }
+ }
+
+ /// <summary>
+ /// History is a reference to the UndoHistory that contains this transaction.
+ /// </summary>
+ public ITextUndoHistory History
+ {
+ get { return this.history; }
+ }
+
+ /// <summary>
+ /// UndoPrimitives allows access to the list of primitives in this transaction container, but should only be called
+ /// after the transaction has been completed.
+ /// </summary>
+ public IList<ITextUndoPrimitive> UndoPrimitives
+ {
+ get
+ {
+ if (this.IsReadOnly)
+ return this.primitives.AsReadOnly();
+ else
+ return this.primitives;
+ }
+ }
+
+ /// <summary>
+ /// Complete marks the transaction finished and eligible for Undo.
+ /// </summary>
+ public void Complete()
+ {
+ if (this.State != UndoTransactionState.Open)
+ {
+ throw new InvalidOperationException("Strings.CompleteCalledOnTransationThatIsNotOpened");
+ }
+
+ this.state = UndoTransactionState.Completed;
+
+ // now we need to pump these primitives into the parent, if the parent exists.
+ FlattenPrimitivesToParent();
+ }
+
+ /// <summary>
+ /// This is called by the transaction when it is complete. It results in the parent getting
+ /// all of this transaction's undo history, so that transactions are not really recursive (they
+ /// exist for rollback).
+ /// </summary>
+ public void FlattenPrimitivesToParent()
+ {
+ if (this.parent != null)
+ {
+ // first, copy up each primitive.
+ this.parent.CopyPrimitivesFrom(this);
+
+ // once all the primitives are in the parent, just clear them so
+ // no one has a chance to tweak them here, or do/undo us.
+ this.primitives.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Copies all of the primitives from the given transaction, and appends them to the UndoPrimitives list.
+ /// </summary>
+ /// <param name="transaction">The UndoTransactionImpl to copy from.</param>
+ public void CopyPrimitivesFrom(UndoTransactionImpl transaction)
+ {
+ foreach (ITextUndoPrimitive p in transaction.UndoPrimitives)
+ {
+ this.AddUndo(p);
+ }
+ }
+
+ /// <summary>
+ /// Cancel marks an Open transaction Canceled, and Undoes and clears any primitives that have been added.
+ /// </summary>
+ public void Cancel()
+ {
+ if (this.State != UndoTransactionState.Open)
+ {
+ throw new InvalidOperationException("Strings.CancelCalledOnTransationThatIsNotOpened");
+ }
+
+ for (int i = primitives.Count - 1; i >= 0; --i)
+ {
+ primitives[i].Undo();
+ }
+
+ this.primitives.Clear();
+ this.state = UndoTransactionState.Canceled;
+ }
+
+ /// <summary>
+ /// AddUndo adds a new primitive to the end of the list when the transaction is Open.
+ /// </summary>
+ /// <param name="undo"></param>
+ public void AddUndo(ITextUndoPrimitive undo)
+ {
+ if (State != UndoTransactionState.Open)
+ {
+ throw new InvalidOperationException("Strings.AddUndoCalledOnTransationThatIsNotOpened");
+ }
+
+ this.primitives.Add(undo);
+ undo.Parent = this;
+
+ MergeMostRecentUndoPrimitive();
+ }
+
+ /// <summary>
+ /// This is called by AddUndo, so that primitives are always in a fully merged state as we go.
+ /// </summary>
+ protected void MergeMostRecentUndoPrimitive()
+ {
+ // no merging unless there are at least two items
+ if (primitives.Count < 2)
+ {
+ return;
+ }
+
+ ITextUndoPrimitive top = primitives[primitives.Count - 1];
+
+ ITextUndoPrimitive victim = null;
+ int victimIndex = -1;
+
+ for (int i = primitives.Count - 2; i >= 0; --i)
+ {
+ if (top.GetType() == primitives[i].GetType() && top.CanMerge(primitives[i]))
+ {
+ victim = primitives[i];
+ victimIndex = i;
+ break;
+ }
+ }
+
+ if (victim != null)
+ {
+ ITextUndoPrimitive newPrimitive = top.Merge(victim);
+ primitives.RemoveRange(primitives.Count - 1, 1);
+ primitives.RemoveRange(victimIndex, 1);
+ primitives.Add(newPrimitive);
+ }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ public ITextUndoTransaction Parent
+ {
+ get { return parent; }
+ }
+
+ /// <summary>
+ /// This is true iff every contained primitive is CanRedo and we are in an Undone state.
+ /// </summary>
+ public bool CanRedo
+ {
+ get
+ {
+ if (this.state == UndoTransactionState.Invalid)
+ {
+ return true;
+ }
+
+ if (this.State != UndoTransactionState.Undone)
+ {
+ return false;
+ }
+
+ foreach (ITextUndoPrimitive primitive in UndoPrimitives)
+ {
+ if (!primitive.CanRedo)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// This is true iff every contained primitive is CanUndo and we are in a Completed state.
+ /// </summary>
+ public bool CanUndo
+ {
+ get
+ {
+ if (this.state == UndoTransactionState.Invalid)
+ {
+ return true;
+ }
+
+ if (this.State != UndoTransactionState.Completed)
+ {
+ return false;
+ }
+
+ foreach (ITextUndoPrimitive primitive in UndoPrimitives)
+ {
+ if (!primitive.CanUndo)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ public void Do()
+ {
+ if (this.state == UndoTransactionState.Invalid)
+ {
+ return;
+ }
+
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException("Strings.DoCalledButCanRedoFalse");
+ }
+
+ this.state = UndoTransactionState.Redoing;
+
+ for (int i = 0; i < primitives.Count; ++i)
+ {
+ primitives[i].Do();
+ }
+
+ this.state = UndoTransactionState.Completed;
+ }
+
+ /// <summary>
+ /// This defers to the linked transaction if there is one.
+ /// </summary>
+ public void Undo()
+ {
+ if (this.state == UndoTransactionState.Invalid)
+ {
+ return;
+ }
+
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException("Strings.UndoCalledButCanUndoFalse");
+ }
+
+ this.state = UndoTransactionState.Undoing;
+
+ for (int i = primitives.Count - 1; i >= 0; --i)
+ {
+ primitives[i].Undo();
+ }
+
+ this.state = UndoTransactionState.Undone;
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ public IMergeTextUndoTransactionPolicy MergePolicy
+ {
+ get { return this.mergePolicy; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ this.mergePolicy = value;
+ }
+ }
+
+ /// <summary>
+ /// Closes a transaction and disposes it.
+ /// </summary>
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ switch (this.State)
+ {
+ case UndoTransactionState.Open:
+ Cancel();
+ break;
+
+ case UndoTransactionState.Canceled:
+ case UndoTransactionState.Completed:
+ break;
+
+ case UndoTransactionState.Redoing:
+ case UndoTransactionState.Undoing:
+ case UndoTransactionState.Undone:
+ throw new InvalidOperationException("Strings.ClosingAnOpenTransactionThatAppearsToBeUndoneOrUndoing");
+ }
+ history.EndTransaction(this);
+ }
+ }
+}
diff --git a/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs b/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs
new file mode 100644
index 0000000..2aba367
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs
@@ -0,0 +1,16 @@
+//
+// 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.Operations.Standalone
+{
+ /// <summary>
+ /// This is the delegate that we ultimately call to perform the work of the
+ /// delegated undo operations. It contains information about all the parameter
+ /// objects as well as the history of origin.
+ /// </summary>
+ internal delegate void UndoableOperationCurried();
+}
diff --git a/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs b/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs
new file mode 100644
index 0000000..e0971d6
--- /dev/null
+++ b/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs
@@ -0,0 +1,105 @@
+//
+// 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.
+//
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.VisualStudio.Text.Operations.Standalone
+{
+ /// <summary>
+ /// This class is the same as WeakReference, except for its implementation of Object.GetHashCode() and Object.Equals().
+ /// In both of those cases, it makes a best attempt at emulating the behavior of the target object, so that when
+ /// WeakReferences are placed within a dictionary, as key values, they are properly recognized as references to objects
+ /// that are the different (or the same).
+ /// </summary>
+ [Serializable]
+ public class WeakReferenceForDictionaryKey : WeakReference
+ {
+ private readonly int hashCode;
+ private const int xorWeakReference = 0xCCCC;
+
+ public WeakReferenceForDictionaryKey(object target)
+ : base(target)
+ {
+ if (target != null)
+ hashCode = target.GetHashCode() ^ xorWeakReference;
+ }
+
+ protected WeakReferenceForDictionaryKey(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ return;
+ }
+
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData(info, context);
+ return ;
+ }
+
+ /// <summary>
+ /// This hash code is informed by (but not the same as) the target object, and is constant for the life
+ /// of the WeakReference.
+ /// </summary>
+ /// <returns>A hash code for the current Object.</returns>
+ public override int GetHashCode()
+ {
+ return hashCode;
+ }
+
+ /// <summary>
+ /// Equality is trickier than the Hashcode, because of the possibility that the target of either WeakReference
+ /// has been GC'ed or finalized. In those cases, when it is not known that the references must have refered to
+ /// the same object, Equals() returns false. Otherwise, it returns the same as the underlying Equals in the
+ /// target objects.
+ /// </summary>
+ /// <param name="obj">The Object to compare with the current Object</param>
+ /// <returns>true if the specified Object is equal to the current Object; otherwise false.</returns>
+ public override bool Equals(object obj)
+ {
+ bool result = false;
+ WeakReferenceForDictionaryKey other = obj as WeakReferenceForDictionaryKey;
+
+ if (Object.ReferenceEquals(other, null))
+ {
+ result = false;
+ }
+ else if (Object.ReferenceEquals(this, other))
+ {
+ result = true;
+ }
+ else
+ {
+ object thisObj = null;
+ object otherObj = null;
+
+ try
+ {
+ thisObj = this.Target;
+ otherObj = other.Target;
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ if (thisObj == null || otherObj == null)
+ {
+ // these objects either refered to null (is that even possible?) or refer to
+ // something that has been GC'ed/finalized. the latter case is the important
+ // scenario for us, and we'll return false, in effect causing each GC'ed
+ // weak reference to appear to be equal only to itself.
+ result = false;
+ }
+ else
+ {
+ result = Object.Equals(thisObj, otherObj);
+ }
+ }
+
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs b/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs
new file mode 100644
index 0000000..910cc91
--- /dev/null
+++ b/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs
@@ -0,0 +1,25 @@
+//
+// 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.Tagging
+{
+ using System.Collections.Generic;
+ using System.ComponentModel;
+
+ /// <summary>
+ /// The metadata interface for exporters and importers of metadata on <see cref="IViewTaggerProvider"/> factories.
+ /// </summary>
+ public interface IViewTaggerMetadata : INamedTaggerMetadata
+ {
+ /// <summary>
+ /// Text view roles to which the tagger provider applies. Default value of null is provided for backward
+ /// compatibility.
+ /// </summary>
+ [DefaultValue(null)]
+ IEnumerable<string> TextViewRoles { get; }
+ }
+}
diff --git a/src/Text/Impl/TagAggregator/TagAggregator.cs b/src/Text/Impl/TagAggregator/TagAggregator.cs
new file mode 100644
index 0000000..2f84741
--- /dev/null
+++ b/src/Text/Impl/TagAggregator/TagAggregator.cs
@@ -0,0 +1,708 @@
+//
+// 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.Tagging.Implementation
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// A tag aggregator gathers, projects, and aggregates tags over a given buffer graph. Consumers
+ /// of tags will get a TagAggregator for a view (likely) or a specific buffer (less likely) and
+ /// query for tags over this aggregator. When finished, consumers are expected to Dispose the
+ /// aggregator, so it can clean up any taggers that are disposable and any cached state it may
+ /// have.
+ /// </summary>
+ internal sealed class TagAggregator<T> : IAccurateTagAggregator<T> where T : ITag
+ {
+ internal TagAggregatorFactoryService TagAggregatorFactoryService { get; private set; }
+ internal IDictionary<ITextBuffer, IList<ITagger<T>>> taggers;
+ private readonly TagAggregatorOptions options;
+ private List<Tuple<ITagger<T>, int>> uniqueTaggers;
+ internal ITextView textView; // can be null
+ internal JoinableTaskHelper joinableTaskHelper;
+
+ internal MappingSpanLink acculumatedSpanLinks = null;
+
+ internal bool disposed;
+ internal bool initialized;
+
+
+ public TagAggregator(TagAggregatorFactoryService factory, ITextView textView, IBufferGraph bufferGraph, TagAggregatorOptions options)
+ {
+ this.TagAggregatorFactoryService = factory;
+ this.textView = textView;
+ this.BufferGraph = bufferGraph;
+ this.options = options;
+ this.joinableTaskHelper = new JoinableTaskHelper(factory.JoinableTaskContext);
+
+ if (textView != null)
+ {
+ textView.Closed += this.OnTextView_Closed;
+ }
+
+ taggers = new Dictionary<ITextBuffer, IList<ITagger<T>>>();
+ uniqueTaggers = new List<Tuple<ITagger<T>, int>>();
+
+ if (((TagAggregatorOptions2)options).HasFlag(TagAggregatorOptions2.DeferTaggerCreation))
+ {
+ this.joinableTaskHelper.RunOnUIThread((Action)(this.EnsureInitialized));
+ }
+ else
+ {
+ this.Initialize();
+ }
+
+ this.BufferGraph.GraphBufferContentTypeChanged += new EventHandler<GraphBufferContentTypeChangedEventArgs>(BufferGraph_GraphBufferContentTypeChanged);
+ this.BufferGraph.GraphBuffersChanged += new EventHandler<GraphBuffersChangedEventArgs>(BufferGraph_GraphBuffersChanged);
+ }
+
+ private void Initialize()
+ {
+ if (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection))
+ {
+ this.taggers[this.BufferGraph.TopBuffer] = GatherTaggers(this.BufferGraph.TopBuffer);
+ }
+ else
+ {
+ //Construct our initial list of taggers by getting taggers for every textBuffer in the graph
+ this.BufferGraph.GetTextBuffers(delegate (ITextBuffer buffer)
+ {
+ this.taggers[buffer] = GatherTaggers(buffer);
+ return false;
+ });
+ }
+
+ this.initialized = true;
+ }
+
+ private void EnsureInitialized()
+ {
+ if (!(this.disposed || this.initialized))
+ {
+ this.Initialize();
+
+ //Raise the tags changed event over the entire buffer since we didn't give the correct results
+ //to anyone who might have called GetTags() before.
+ ITextSnapshot snapshot = this.BufferGraph.TopBuffer.CurrentSnapshot;
+ IMappingSpan span = this.BufferGraph.CreateMappingSpan(new SnapshotSpan(snapshot, 0, snapshot.Length), SpanTrackingMode.EdgeInclusive);
+
+ this.RaiseEvents(this, span);
+ }
+ }
+
+ #region ITagAggregator<T> Members
+
+ public IBufferGraph BufferGraph { get; private set; }
+
+ public IEnumerable<IMappingTagSpan<T>> GetTags(SnapshotSpan span)
+ {
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ if (this.uniqueTaggers.Count == 0)
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ else
+ {
+ return InternalGetTags(new NormalizedSnapshotSpanCollection(span), cancel: null);
+ }
+ }
+
+ public IEnumerable<IMappingTagSpan<T>> GetTags(IMappingSpan span)
+ {
+ if (span == null)
+ throw new ArgumentNullException("span");
+
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ if (this.uniqueTaggers.Count == 0)
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ else
+ {
+ return InternalGetTags(span, cancel: null);
+ }
+ }
+
+ public IEnumerable<IMappingTagSpan<T>> GetTags(NormalizedSnapshotSpanCollection snapshotSpans)
+ {
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ if ((this.uniqueTaggers.Count > 0) && (snapshotSpans.Count > 0))
+ {
+ return InternalGetTags(snapshotSpans, cancel: null);
+ }
+ else
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ }
+
+ public event EventHandler<TagsChangedEventArgs> TagsChanged;
+
+ public event EventHandler<BatchedTagsChangedEventArgs> BatchedTagsChanged;
+
+ #endregion
+
+ #region IAccurateTagAggregator<T> Members
+
+ public IEnumerable<IMappingTagSpan<T>> GetAllTags(SnapshotSpan span, CancellationToken cancel)
+ {
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ this.EnsureInitialized();
+
+ if (this.uniqueTaggers.Count == 0)
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ else
+ {
+ return InternalGetTags(new NormalizedSnapshotSpanCollection(span), cancel);
+ }
+ }
+
+ public IEnumerable<IMappingTagSpan<T>> GetAllTags(IMappingSpan span, CancellationToken cancel)
+ {
+ if (span == null)
+ throw new ArgumentNullException("span");
+
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ this.EnsureInitialized();
+
+ if (this.uniqueTaggers.Count == 0)
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ else
+ {
+ return InternalGetTags(span, cancel);
+ }
+ }
+
+ public IEnumerable<IMappingTagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken cancel)
+ {
+ if (this.disposed)
+ throw new ObjectDisposedException("TagAggregator");
+
+ this.EnsureInitialized();
+
+ if ((this.uniqueTaggers.Count > 0) && (snapshotSpans.Count > 0))
+ {
+ return InternalGetTags(snapshotSpans, cancel);
+ }
+ else
+ {
+ return Enumerable.Empty<IMappingTagSpan<T>>();
+ }
+ }
+ #endregion
+
+ #region IDisposable Members
+ public void Dispose()
+ {
+ if (this.disposed)
+ return;
+
+ try
+ {
+ if (this.textView != null)
+ this.textView.Closed -= this.OnTextView_Closed;
+
+ this.BufferGraph.GraphBufferContentTypeChanged -= BufferGraph_GraphBufferContentTypeChanged;
+ this.BufferGraph.GraphBuffersChanged -= BufferGraph_GraphBuffersChanged;
+
+ this.DisposeAllTaggers();
+ }
+ finally
+ {
+ this.taggers = null;
+ this.TagAggregatorFactoryService = null;
+ this.BufferGraph = null;
+ this.textView = null;
+ this.uniqueTaggers = null;
+
+ disposed = true;
+ }
+ }
+ #endregion
+
+ #region Event Handlers
+ /// <summary>
+ /// When a source tagger sends out a change event, we translate the SnapshotSpan
+ /// that was changed into a mapping span for our consumers.
+ /// </summary>
+ void SourceTaggerTagsChanged(object sender, SnapshotSpanEventArgs e)
+ {
+ if (this.disposed)
+ return;
+
+ // Create a mapping span for the region and return that in our own event
+ IMappingSpan span = this.BufferGraph.CreateMappingSpan(e.Span, SpanTrackingMode.EdgeExclusive);
+
+ RaiseEvents(sender, span);
+ }
+
+ private void RaiseEvents(object sender, IMappingSpan span)
+ {
+ EventHandler<TagsChangedEventArgs> tempEvent = TagsChanged;
+ if (tempEvent != null)
+ {
+ this.TagAggregatorFactoryService.GuardedOperations.RaiseEvent(sender, tempEvent, new TagsChangedEventArgs(span));
+ }
+
+ if (this.BatchedTagsChanged != null)
+ {
+ var oldHead = Volatile.Read(ref this.acculumatedSpanLinks);
+ while (true)
+ {
+ var newHead = new MappingSpanLink(oldHead, span);
+ var result = Interlocked.CompareExchange(ref this.acculumatedSpanLinks, newHead, oldHead);
+ if (result == oldHead)
+ {
+ if (oldHead == null)
+ {
+ this.joinableTaskHelper.RunOnUIThread((Action)(this.RaiseBatchedTagsChanged));
+ }
+
+ break;
+ }
+
+ oldHead = result;
+ }
+ }
+ }
+
+ private void RaiseBatchedTagsChanged()
+ {
+ // We may have been disposed between when the event was
+ // dispatched and now; if so, just quit.
+ if (this.disposed)
+ return;
+
+ bool raiseEvent = true;
+
+ EventHandler<BatchedTagsChangedEventArgs> tempEvent = this.BatchedTagsChanged;
+ if (tempEvent != null)
+ {
+ if (this.textView != null)
+ {
+ if (this.textView.IsClosed)
+ {
+ // There's no need to actually raise the event (this probably won't happen since -- with a closed view -- there shouldn't be any listeners).
+ raiseEvent = false;
+ }
+ else if (this.textView.InLayout)
+ {
+ // The view is in the middle of a layout (because someone was pumping messages while handling a call from inside a layout).
+ // Many BatchTagsChanged handlers will not handle that situation gracefully so simply delay raising the event until
+ // we're no longer inside a layout.
+ this.joinableTaskHelper.RunOnUIThread((Action)(this.RaiseBatchedTagsChanged));
+
+ return;
+ }
+ }
+ }
+ else
+ {
+ raiseEvent = false;
+ }
+
+ var oldHead = Volatile.Read(ref this.acculumatedSpanLinks);
+ while (true)
+ {
+ var result = Interlocked.CompareExchange(ref this.acculumatedSpanLinks, null, oldHead);
+ if (result == oldHead)
+ {
+ if (raiseEvent)
+ {
+ var spans = new List<IMappingSpan>(oldHead.Count);
+ do
+ {
+ spans.Add(oldHead.Span);
+ oldHead = oldHead.Next;
+ }
+ while (oldHead != null);
+
+ this.TagAggregatorFactoryService.GuardedOperations.RaiseEvent(this, tempEvent, new BatchedTagsChangedEventArgs(spans));
+ }
+
+ break;
+ }
+
+ oldHead = result;
+ }
+ }
+
+ internal class MappingSpanLink
+ {
+ public readonly MappingSpanLink Next;
+ public readonly IMappingSpan Span;
+ public int Count { get { return (this.Next == null) ? 1 : (this.Next.Count + 1); } }
+
+ public MappingSpanLink(MappingSpanLink next, IMappingSpan span)
+ {
+ this.Next = next;
+ this.Span = span;
+ }
+ }
+
+ /// <summary>
+ /// When buffers are added or removed from the buffer graph, we (1) dispose all
+ /// the removed buffers' taggers (if they are disposable) and (2) collect all
+ /// taggers on the new buffers.
+ /// </summary>
+ void BufferGraph_GraphBuffersChanged(object sender, GraphBuffersChangedEventArgs e)
+ {
+ if (this.disposed || (!this.initialized) || (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection)))
+ return;
+
+ foreach (ITextBuffer buffer in e.RemovedBuffers)
+ {
+ DisposeAllTaggersOverBuffer(buffer);
+ taggers.Remove(buffer);
+ }
+
+ foreach (ITextBuffer buffer in e.AddedBuffers)
+ {
+ taggers[buffer] = GatherTaggers(buffer);
+ }
+ }
+
+ /// <summary>
+ /// If the content type of any of the source buffers changes, we need to dispose
+ /// all the taggers on the buffer that we have cached (if they are disposable) and get
+ /// new ones.
+ /// </summary>
+ void BufferGraph_GraphBufferContentTypeChanged(object sender, GraphBufferContentTypeChangedEventArgs e)
+ {
+ if (this.disposed || !this.initialized || (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection) && (e.TextBuffer != this.BufferGraph.TopBuffer)))
+ return;
+
+ DisposeAllTaggersOverBuffer(e.TextBuffer);
+ taggers[e.TextBuffer] = GatherTaggers(e.TextBuffer);
+
+ // Send out an event to say that tags have changed over the entire text buffer, to
+ // be safe.
+ ITextSnapshot snapshot = e.TextBuffer.CurrentSnapshot;
+ SnapshotSpan entireSnapshot = new SnapshotSpan(snapshot, 0, snapshot.Length);
+ IMappingSpan span = this.BufferGraph.CreateMappingSpan(entireSnapshot, SpanTrackingMode.EdgeInclusive);
+
+ this.RaiseEvents(this, span);
+ }
+
+ private void OnTextView_Closed(object sender, EventArgs args)
+ {
+ this.Dispose();
+ }
+ #endregion
+
+ #region Helpers
+ private IEnumerable<IMappingTagSpan<T>> GetTagsForBuffer(KeyValuePair<ITextBuffer, IList<ITagger<T>>> bufferAndTaggers,
+ NormalizedSnapshotSpanCollection snapshotSpans,
+ ITextSnapshot root, CancellationToken? cancel)
+ {
+ ITextSnapshot snapshot = snapshotSpans[0].Snapshot;
+
+ for (int t = 0; t < bufferAndTaggers.Value.Count; ++t)
+ {
+ ITagger<T> tagger = bufferAndTaggers.Value[t];
+ IEnumerator<ITagSpan<T>> tags = null;
+ try
+ {
+ IEnumerable<ITagSpan<T>> tagEnumerable;
+
+ if (cancel.HasValue)
+ {
+ cancel.Value.ThrowIfCancellationRequested();
+
+ var tagger2 = tagger as IAccurateTagger<T>;
+ if (tagger2 != null)
+ {
+ tagEnumerable = tagger2.GetAllTags(snapshotSpans, cancel.Value);
+ }
+ else
+ {
+ tagEnumerable = tagger.GetTags(snapshotSpans);
+ }
+ }
+ else
+ {
+ tagEnumerable = tagger.GetTags(snapshotSpans);
+ }
+
+ if (tagEnumerable != null)
+ tags = tagEnumerable.GetEnumerator();
+ }
+ catch (OperationCanceledException)
+ {
+ // Rethrow cancellation exceptions since we expect our callers to deal with it.
+ throw;
+ }
+ catch (Exception e)
+ {
+ this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e);
+ }
+
+ if (tags != null)
+ {
+ try
+ {
+ while (true)
+ {
+ ITagSpan<T> tagSpan = null;
+ try
+ {
+ if (tags.MoveNext())
+ tagSpan = tags.Current;
+ }
+ catch (Exception e)
+ {
+ this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e);
+ }
+
+ if (tagSpan == null)
+ break;
+
+ var snapshotSpan = tagSpan.Span;
+
+ if (snapshotSpans.IntersectsWith(snapshotSpan.TranslateTo(snapshot, SpanTrackingMode.EdgeExclusive)))
+ {
+ yield return new MappingTagSpan<T>(
+ (root == null)
+ ? this.BufferGraph.CreateMappingSpan(snapshotSpan, SpanTrackingMode.EdgeExclusive)
+ : MappingSpanSnapshot.Create(root, snapshotSpan, SpanTrackingMode.EdgeExclusive, this.BufferGraph),
+ tagSpan.Tag);
+ }
+ else
+ {
+#if DEBUG
+ Debug.WriteLine("tagger provided an extra (non-intersecting) tag at " + snapshotSpan + " when queried for tags over " + snapshotSpans);
+#endif
+ }
+ }
+ }
+ finally
+ {
+ try
+ {
+ tags.Dispose();
+ }
+ catch (Exception e)
+ {
+ this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e);
+ }
+ }
+ }
+ }
+ }
+
+ private IEnumerable<IMappingTagSpan<T>> InternalGetTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken? cancel)
+ {
+ ITextSnapshot targetSnapshot = snapshotSpans[0].Snapshot;
+
+ bool mapByContentType = (options & TagAggregatorOptions.MapByContentType) != 0;
+
+ foreach (var bufferAndTaggers in taggers)
+ {
+ if (bufferAndTaggers.Value.Count > 0)
+ {
+ FrugalList<SnapshotSpan> targetSpans = new FrugalList<SnapshotSpan>();
+ for (int s = 0; s < snapshotSpans.Count; ++s)
+ {
+ MappingHelper.MapDownToBufferNoTrack(snapshotSpans[s], bufferAndTaggers.Key, targetSpans, mapByContentType);
+ }
+
+ if (targetSpans.Count > 0)
+ {
+ NormalizedSnapshotSpanCollection targetSpanCollection =
+ new NormalizedSnapshotSpanCollection(targetSpans);
+
+ foreach (var tagSpan in this.GetTagsForBuffer(bufferAndTaggers, targetSpanCollection, targetSnapshot, cancel))
+ {
+ yield return tagSpan;
+ }
+ }
+ }
+ }
+ }
+
+ private IEnumerable<IMappingTagSpan<T>> InternalGetTags(IMappingSpan mappingSpan, CancellationToken? cancel)
+ {
+ foreach (var bufferAndTaggers in taggers)
+ {
+ if (bufferAndTaggers.Value.Count > 0)
+ {
+ NormalizedSnapshotSpanCollection spans = mappingSpan.GetSpans(bufferAndTaggers.Key);
+
+ if (spans.Count > 0)
+ {
+ foreach (var tagSpan in this.GetTagsForBuffer(bufferAndTaggers, spans, null, cancel))
+ {
+ yield return tagSpan;
+ }
+ }
+ }
+ }
+ }
+
+ void DisposeAllTaggers()
+ {
+ foreach (var bufferAndTaggers in taggers)
+ {
+ DisposeAllTaggersOverBuffer(bufferAndTaggers.Value);
+ }
+ }
+
+ void DisposeAllTaggersOverBuffer(ITextBuffer buffer)
+ {
+ DisposeAllTaggersOverBuffer(taggers[buffer]);
+ }
+
+ void DisposeAllTaggersOverBuffer(IList<ITagger<T>> taggersOnBuffer)
+ {
+ foreach (ITagger<T> tagger in taggersOnBuffer)
+ {
+ this.UnregisterTagger(tagger);
+ }
+ }
+
+ internal IList<ITagger<T>> GatherTaggers(ITextBuffer textBuffer)
+ {
+ List<ITagger<T>> newTaggers = new List<ITagger<T>>();
+
+ var bufferTaggerFactories = this.TagAggregatorFactoryService.GuardedOperations.FindEligibleFactories(this.TagAggregatorFactoryService.GetBufferTaggersForType(textBuffer.ContentType, typeof(T)),
+ textBuffer.ContentType,
+ this.TagAggregatorFactoryService.ContentTypeRegistryService);
+
+ foreach (var factory in bufferTaggerFactories)
+ {
+ ITaggerProvider provider = null;
+ ITagger<T> tagger = null;
+
+ try
+ {
+ provider = factory.Value;
+ tagger = provider.CreateTagger<T>(textBuffer);
+ }
+ catch (Exception e)
+ {
+ object errorSource = (provider != null) ? (object)provider : factory;
+ this.TagAggregatorFactoryService.GuardedOperations.HandleException(errorSource, e);
+ }
+
+ this.RegisterTagger(tagger, newTaggers);
+ }
+
+ if (this.textView != null)
+ {
+ var viewTaggerFactories = this.TagAggregatorFactoryService.GuardedOperations.FindEligibleFactories(this.TagAggregatorFactoryService.GetViewTaggersForType(textBuffer.ContentType, typeof(T)).Where(f =>
+ (f.Metadata.TextViewRoles == null) || this.textView.Roles.ContainsAny(f.Metadata.TextViewRoles)),
+ textBuffer.ContentType,
+ this.TagAggregatorFactoryService.ContentTypeRegistryService);
+
+ foreach (var factory in viewTaggerFactories)
+ {
+ IViewTaggerProvider provider = null;
+ ITagger<T> tagger = null;
+
+ try
+ {
+ provider = factory.Value;
+ tagger = provider.CreateTagger<T>(this.textView, textBuffer);
+ }
+ catch (Exception e)
+ {
+ object errorSource = (provider != null) ? (object)provider : factory;
+ this.TagAggregatorFactoryService.GuardedOperations.HandleException(errorSource, e);
+ }
+
+ this.RegisterTagger(tagger, newTaggers);
+ }
+ }
+
+ return newTaggers;
+ }
+
+ private void UnregisterTagger(ITagger<T> tagger)
+ {
+ int taggerIndex = uniqueTaggers.FindIndex((tuple) => object.ReferenceEquals(tuple.Item1, tagger));
+
+ if (taggerIndex != -1)
+ {
+ Tuple<ITagger<T>, int> taggerData = this.uniqueTaggers[taggerIndex];
+
+ // Is there only one reference remaining for this item?
+ if (taggerData.Item2 == 1)
+ {
+ tagger.TagsChanged -= SourceTaggerTagsChanged;
+
+ this.uniqueTaggers.RemoveAt(taggerIndex);
+ }
+ else
+ {
+ // Decrease the ref count of the tagger by 1
+ this.uniqueTaggers[taggerIndex] = Tuple.Create(tagger, taggerData.Item2 - 1);
+ }
+ }
+ else
+ {
+ Debug.Fail("The tagger should still be in the list of unique taggers.");
+ }
+
+ IDisposable disposable = tagger as IDisposable;
+ if (disposable != null)
+ {
+ this.TagAggregatorFactoryService.GuardedOperations.CallExtensionPoint(this, () => disposable.Dispose());
+ }
+ }
+
+ private void RegisterTagger(ITagger<T> tagger, IList<ITagger<T>> newTaggers)
+ {
+ if (tagger != null)
+ {
+ newTaggers.Add(tagger);
+
+ int taggerIndex = this.uniqueTaggers.FindIndex((tuple) => object.ReferenceEquals(tuple.Item1, tagger));
+
+ // Only subscribe to the event if we've never seen this tagger before
+ if (taggerIndex == -1)
+ {
+ tagger.TagsChanged += SourceTaggerTagsChanged;
+
+ uniqueTaggers.Add(Tuple.Create(tagger, 1));
+ }
+ else
+ {
+ // Increase the reference count for the existing tagger
+ uniqueTaggers[taggerIndex] = Tuple.Create(tagger, uniqueTaggers[taggerIndex].Item2 + 1);
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs
new file mode 100644
index 0000000..1b743a5
--- /dev/null
+++ b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs
@@ -0,0 +1,168 @@
+//
+// 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.Tagging.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.ComponentModel.Composition;
+ using System.Linq;
+
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Threading;
+
+ /// <summary>
+ /// Exports the TagAggregator provider, both the buffer and view version.
+ /// </summary>
+ [Export(typeof(IBufferTagAggregatorFactoryService))]
+ [Export(typeof(IViewTagAggregatorFactoryService))]
+ internal sealed class TagAggregatorFactoryService : IBufferTagAggregatorFactoryService, IViewTagAggregatorFactoryService
+ {
+ [ImportMany(typeof(ITaggerProvider))]
+ internal List<Lazy<ITaggerProvider, INamedTaggerMetadata>> BufferTaggerProviders { get; set; }
+
+ [ImportMany(typeof(IViewTaggerProvider))]
+ internal List<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> ViewTaggerProviders { get; set; }
+
+ [Import]
+ internal IBufferGraphFactoryService BufferGraphFactoryService { get; set; }
+
+ [Import]
+ internal IContentTypeRegistryService ContentTypeRegistryService { get; set; }
+
+ [Import]
+ internal JoinableTaskContext JoinableTaskContext { get; set; }
+
+ [Import]
+ internal GuardedOperations GuardedOperations { get; set; }
+
+ internal ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>>> _bufferTaggerProviderMap = ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>>>.Empty;
+ internal ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>> _viewTaggerProviderMap = ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>>.Empty;
+
+ #region IBufferTagAggregatorFactoryService Members
+
+ public ITagAggregator<T> CreateTagAggregator<T>(ITextBuffer textBuffer) where T : ITag
+ {
+ return CreateTagAggregator<T>(textBuffer, TagAggregatorOptions.None);
+ }
+
+ public ITagAggregator<T> CreateTagAggregator<T>(ITextBuffer textBuffer, TagAggregatorOptions options) where T : ITag
+ {
+ if (textBuffer == null)
+ throw new ArgumentNullException("textBuffer");
+
+ return new TagAggregator<T>(this, null, this.BufferGraphFactoryService.CreateBufferGraph(textBuffer), options);
+
+ }
+
+ #endregion
+
+ #region IViewTagAggregatorFactoryService Members
+
+ public ITagAggregator<T> CreateTagAggregator<T>(ITextView textView) where T : ITag
+ {
+ return CreateTagAggregator<T>(textView, TagAggregatorOptions.None);
+ }
+
+ public ITagAggregator<T> CreateTagAggregator<T>(ITextView textView, TagAggregatorOptions options) where T : ITag
+ {
+ if (textView == null)
+ throw new ArgumentNullException("textView");
+
+ return new TagAggregator<T>(this, textView, textView.BufferGraph, options);
+ }
+
+ #endregion
+
+ internal IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>> GetBufferTaggersForType(IContentType type, Type taggerType)
+ {
+ var key = new ContentAndTypeData(type, taggerType);
+
+ IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>> taggers;
+ if (!_bufferTaggerProviderMap.TryGetValue(key, out taggers))
+ {
+ taggers = new List<Lazy<ITaggerProvider, INamedTaggerMetadata>>(this.BufferTaggerProviders.Where(f => Match(type, taggerType, f.Metadata)));
+
+ ImmutableInterlocked.Update(ref _bufferTaggerProviderMap, (s) => s.Add(key, taggers));
+ }
+
+ return taggers;
+ }
+
+ internal IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> GetViewTaggersForType(IContentType type, Type taggerType)
+ {
+ var key = new ContentAndTypeData(type, taggerType);
+
+ IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> taggers;
+ if (!_viewTaggerProviderMap.TryGetValue(key, out taggers))
+ {
+ taggers = new List<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>(this.ViewTaggerProviders.Where(f => Match(type, taggerType, f.Metadata)));
+
+ ImmutableInterlocked.Update(ref _viewTaggerProviderMap, (s) => s.Add(key, taggers));
+ }
+
+ return taggers;
+ }
+
+ private static bool Match(IContentType bufferContentType, Type taggerType, INamedTaggerMetadata tagMetadata)
+ {
+ bool contentTypeMatch = false;
+
+ foreach (string contentType in tagMetadata.ContentTypes)
+ {
+ if (bufferContentType.IsOfType(contentType))
+ {
+ contentTypeMatch = true;
+ break;
+ }
+ }
+
+ if (contentTypeMatch)
+ {
+ // Now find out if it can provide tags of the type we want
+ foreach (Type type in tagMetadata.TagTypes)
+ {
+ // This producer is used if it claims to produce a tag
+ // that this type is assignable from.
+ if (taggerType.IsAssignableFrom(type))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ internal class ContentAndTypeData
+ {
+ public readonly IContentType ContentType;
+ public readonly Type TaggerType;
+
+ public ContentAndTypeData(IContentType contentType, Type taggerType)
+ {
+ this.ContentType = contentType;
+ this.TaggerType = taggerType;
+ }
+
+ public override bool Equals(object obj)
+ {
+ var other = obj as ContentAndTypeData;
+ return (other != null) && (other.ContentType == this.ContentType) && (other.TaggerType == this.TaggerType);
+ }
+
+ public override int GetHashCode()
+ {
+ return this.ContentType.GetHashCode() ^ this.TaggerType.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs
new file mode 100644
index 0000000..75dcf7e
--- /dev/null
+++ b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs
@@ -0,0 +1,117 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:2.0.50727.1426
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Logic.Text.BufferUndoManager.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot redo this change..
+ /// </summary>
+ internal static string CannotRedo {
+ get {
+ return ResourceManager.GetString("CannotRedo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot undo this change..
+ /// </summary>
+ internal static string CannotUndo {
+ get {
+ return ResourceManager.GetString("CannotUndo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Create Read Only Region.
+ /// </summary>
+ internal static string ReadOnlyRegionCreated {
+ get {
+ return ResourceManager.GetString("ReadOnlyRegionCreated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Remove Read Only Region.
+ /// </summary>
+ internal static string ReadOnlyRegionRemoved {
+ get {
+ return ResourceManager.GetString("ReadOnlyRegionRemoved", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Text Buffer Change.
+ /// </summary>
+ internal static string TextBufferChanged {
+ get {
+ return ResourceManager.GetString("TextBufferChanged", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to TextVersion has no associated changes.
+ /// </summary>
+ internal static string TextVersionNoChanges {
+ get {
+ return ResourceManager.GetString("TextVersionNoChanges", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextBufferUndoManager/Strings.resx b/src/Text/Impl/TextBufferUndoManager/Strings.resx
new file mode 100644
index 0000000..cafb25d
--- /dev/null
+++ b/src/Text/Impl/TextBufferUndoManager/Strings.resx
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CannotUndo" xml:space="preserve">
+ <value>Cannot undo this change.</value>
+ </data>
+ <data name="CannotRedo" xml:space="preserve">
+ <value>Cannot redo this change.</value>
+ </data>
+ <data name="TextBufferChanged" xml:space="preserve">
+ <value>Text Buffer Change</value>
+ </data>
+ <data name="ReadOnlyRegionCreated" xml:space="preserve">
+ <value>Create Read Only Region</value>
+ </data>
+ <data name="ReadOnlyRegionRemoved" xml:space="preserve">
+ <value>Remove Read Only Region</value>
+ </data>
+ <data name="TextVersionNoChanges" xml:space="preserve">
+ <value>TextVersion has no associated changes</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs
new file mode 100644
index 0000000..b4831e4
--- /dev/null
+++ b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs
@@ -0,0 +1,287 @@
+//
+// 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.BufferUndoManager.Implementation
+{
+ using System;
+ using System.Text;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Operations;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+
+ /// <summary>
+ /// The UndoPrimitive for a text buffer change operation.
+ /// </summary>
+ internal class TextBufferChangeUndoPrimitive : TextUndoPrimitive
+ {
+ #region Private Data Members
+
+ private bool _canUndo;
+
+ private readonly ITextUndoHistory _undoHistory;
+ private WeakReference _weakBufferReference;
+
+ private readonly INormalizedTextChangeCollection _textChanges;
+ private int? _beforeVersion;
+ private int? _afterVersion;
+#if DEBUG
+ private int _bufferLengthAfterChange;
+#endif
+
+ #endregion // Private Data Members
+
+ /// <summary>
+ /// Constructs a TextBufferChangeUndoPrimitive.
+ /// </summary>
+ /// <param name="undoHistory">
+ /// The ITextUndoHistory this change will be added to.
+ /// </param>
+ /// <param name="textVersion">
+ /// The <see cref="ITextVersion" /> representing this change.
+ /// This is actually the version associated with the snapshot prior to the change.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="textVersion"/> is null.</exception>
+ public TextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, ITextVersion textVersion)
+ {
+ // Verify input parameters
+ if (undoHistory == null)
+ {
+ throw new ArgumentNullException("undoHistory");
+ }
+
+ if (textVersion == null)
+ {
+ throw new ArgumentNullException("textVersion");
+ }
+
+ _textChanges = textVersion.Changes;
+ _beforeVersion = textVersion.ReiteratedVersionNumber;
+ _afterVersion = textVersion.Next.VersionNumber;
+ Debug.Assert(textVersion.Next.VersionNumber == textVersion.Next.ReiteratedVersionNumber,
+ "Creating a TextBufferChangeUndoPrimitive for a change that has previously been undone? This is probably wrong.");
+
+ _undoHistory = undoHistory;
+ TextBuffer = textVersion.TextBuffer;
+ AttachedToNewBuffer = false;
+
+ _canUndo = true;
+
+#if DEBUG
+ // for debug sanity checks
+ _bufferLengthAfterChange = textVersion.Next.Length;
+#endif
+ }
+
+ #region UndoPrimitive Members
+
+ /// <summary>
+ /// Returns true if operation can be undone, false otherwise.
+ /// </summary>
+ public override bool CanUndo
+ {
+ get
+ {
+ // NOTE: We don't know for sure if we can undo (it might get blocked by a readonly region or a
+ // canceled edit), in which case the actual Undo() will fail.
+ return _canUndo;
+ }
+ }
+
+ /// <summary>
+ /// Returns true if operation can be redone, false otherwise.
+ /// </summary>
+ public override bool CanRedo
+ {
+ get
+ {
+ // NOTE: We don't know for sure if we can redo (it might get blocked by a readonly region or a
+ // canceled edit), in which case the actual Do() will fail.
+ return !_canUndo;
+ }
+ }
+
+ /// <summary>
+ /// Redo the text buffer change action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception>
+ public override void Do()
+ {
+ // Validate, we shouldn't be allowed to undo
+ if (!CanRedo)
+ {
+ throw new InvalidOperationException(Strings.CannotRedo);
+ }
+
+ // For undo-in-closed-files scenarios where we are done/undone on a buffer other
+ // than the one we were originally created on.
+ if (AttachedToNewBuffer)
+ {
+ AttachedToNewBuffer = false;
+
+ _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber;
+ _afterVersion = null;
+ }
+
+ bool editCanceled = false;
+ using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _afterVersion, typeof(TextBufferChangeUndoPrimitive)))
+ {
+ foreach (ITextChange textChange in _textChanges)
+ {
+ if (!edit.Replace(new Span(textChange.OldPosition, textChange.OldLength), textChange.NewText))
+ {
+ // redo canceled by readonly region
+ editCanceled = true;
+ break;
+ }
+ }
+
+ if (!editCanceled)
+ {
+ edit.Apply();
+
+ if (edit.Canceled)
+ {
+ editCanceled = true;
+ }
+ }
+ }
+
+ if (editCanceled)
+ {
+ throw new OperationCanceledException("Redo failed due to readonly regions or canceled edit.");
+ }
+
+ if (_afterVersion == null)
+ {
+ _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber;
+ }
+
+#if DEBUG
+ // sanity check
+ Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange,
+ "The buffer is in a different state than when this TextBufferChangeUndoPrimitive was created!");
+#endif
+
+ _canUndo = true;
+ }
+
+ /// <summary>
+ /// Undo the text buffer change action.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception>
+ public override void Undo()
+ {
+ // Validate that we can undo this change
+ if (!CanUndo)
+ {
+ throw new InvalidOperationException(Strings.CannotUndo);
+ }
+
+#if DEBUG
+ // sanity check
+ Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange,
+ "The buffer is in a different state than when this TextBufferUndoChangePrimitive was created!");
+#endif
+
+ // For undo-in-closed-files scenarios where we are done/undone on a buffer other
+ // than the one we were originally created on.
+ if (AttachedToNewBuffer)
+ {
+ AttachedToNewBuffer = false;
+
+ _beforeVersion = null;
+ _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber;
+ }
+
+ bool editCanceled = false;
+ using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _beforeVersion, typeof(TextBufferChangeUndoPrimitive)))
+ {
+ foreach (ITextChange textChange in _textChanges)
+ {
+ if (!edit.Replace(new Span(textChange.NewPosition, textChange.NewLength), textChange.OldText))
+ {
+ // undo canceled by readonly region
+ editCanceled = true;
+ break;
+ }
+ }
+
+ if (!editCanceled)
+ {
+ edit.Apply();
+
+ if (edit.Canceled)
+ {
+ editCanceled = true;
+ }
+ }
+ }
+
+ if (editCanceled)
+ {
+ throw new OperationCanceledException("Undo failed due to readonly regions or canceled edit.");
+ }
+
+ if (_beforeVersion == null)
+ {
+ _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber;
+ }
+
+ _canUndo = false;
+ }
+
+ public override bool CanMerge(ITextUndoPrimitive older)
+ {
+ return false;
+ }
+ #endregion
+
+ #region Private Helpers
+ /// <summary>
+ /// We track our ITextBuffer in ITextUndoHistory.Properties so that we can be redirected to act on a
+ /// different ITextBuffer in the undo-in-closed-files scenario.
+ /// </summary>
+ private ITextBuffer TextBuffer
+ {
+ get
+ {
+ ITextBuffer buffer;
+ if (!_undoHistory.Properties.TryGetProperty(typeof(ITextBuffer), out buffer))
+ {
+ Debug.Assert(false);
+ throw new InvalidOperationException("ITextUndoHistory.Properties must contain an entry for the ITextBuffer this TextBufferChangeUndoPrimitive should act against.");
+ }
+
+ return buffer;
+ }
+ set
+ {
+ _undoHistory.Properties[typeof(ITextBuffer)] = value;
+ }
+ }
+
+ private bool AttachedToNewBuffer
+ {
+ get
+ {
+ return _weakBufferReference.Target != TextBuffer;
+ }
+ set
+ {
+ if (value != false)
+ {
+ throw new InvalidOperationException("AttachedToNewBuffer can only be reset to false.");
+ }
+ Debug.Assert(TextBuffer != null);
+ _weakBufferReference = new WeakReference(TextBuffer);
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs
new file mode 100644
index 0000000..9fe2e37
--- /dev/null
+++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs
@@ -0,0 +1,204 @@
+//
+// 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.BufferUndoManager.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Operations;
+ using System.Diagnostics;
+
+ class TextBufferUndoManager : ITextBufferUndoManager, IDisposable
+ {
+ #region Private Members
+
+ ITextBuffer _textBuffer;
+ ITextUndoHistoryRegistry _undoHistoryRegistry;
+ ITextUndoHistory _undoHistory;
+ Queue<ITextVersion> _editVersionList = new Queue<ITextVersion>();
+ bool _inPostChanged;
+
+ #endregion
+
+ public TextBufferUndoManager(ITextBuffer textBuffer, ITextUndoHistoryRegistry undoHistoryRegistry)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ if (undoHistoryRegistry == null)
+ {
+ throw new ArgumentNullException("undoHistoryRegistry");
+ }
+
+ _textBuffer = textBuffer;
+
+ _undoHistoryRegistry = undoHistoryRegistry;
+
+ // Register the undo history
+ _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer);
+
+ // Listen for the buffer changed events so that we can make them undo/redo-able
+ _textBuffer.Changed += TextBufferChanged;
+ _textBuffer.PostChanged += TextBufferPostChanged;
+ _textBuffer.Changing += TextBufferChanging;
+ }
+
+ #region Private Methods
+
+ private void TextBufferChanged(object sender, TextContentChangedEventArgs e)
+ {
+ Debug.Assert((e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive) ||
+ (_undoHistory.State != TextUndoHistoryState.Idle),
+ "We are undoing/redoing a change while UndoHistory.State is Idle. Something is wrong with the state.");
+
+ // If this change didn't originate from undo, add a TextBufferChangeUndoPrimitive to our history.
+ if (_undoHistory.State == TextUndoHistoryState.Idle &&
+ (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive))
+ {
+ // With projection, we sometimes get Changed events with no changes, or for "" -> "".
+ // We don't want to create undo actions for these.
+ bool nonNullChange = false;
+ foreach (ITextChange c in e.BeforeVersion.Changes)
+ {
+ if (c.OldLength != 0 || c.NewLength != 0)
+ {
+ nonNullChange = true;
+ break;
+ }
+ }
+
+ if (nonNullChange)
+ {
+ // Queue the edit, and actually add an undo primitive later (see comment on PostChanged).
+ _editVersionList.Enqueue(e.BeforeVersion);
+ }
+ }
+ }
+
+ /// <remarks>
+ /// Edits are queued up by our TextBufferChanged handler and then we finally add them to the
+ /// undo stack here in response to PostChanged. The reason and history behind why we do this
+ /// is as follows:
+ ///
+ /// Originally this was done for VB commit, which uses undo events (i.e. TransactionCompleted) to
+ /// trigger commit. Their commit logic relies on the buffer being in a state such that applying
+ /// an edit synchronously raises a Changed event (which is always the case for PostChanged, but
+ /// not for Changed if there are nested edits).
+ ///
+ /// JaredPar made a change (CS 1182244) that allowed VB to detect that UndoTransactionCompleted
+ /// was being fired from a nested edit, and therefore delay the actual commit until the following
+ /// PostChanged event.
+ ///
+ /// So this allowed us to move TextBufferUndoManager back to adding undo actions directly
+ /// from the TextBufferChanged handler (CS 1285117). This is preferable, as otherwise there's a
+ /// "delay" between when the edit happens and when we record the edit on the undo stack,
+ /// allowing other people to stick something on the undo stack (i.e. from
+ /// their ITextBuffer.Changed handler) in between. The result is actions being "out-of-order"
+ /// on the undo stack.
+ ///
+ /// Unfortunately, it turns out VB snippets actually rely on this "out-of-order" behavior
+ /// (see Dev10 834740) and so we are forced to revert CS 1285117) and return to the model
+ /// where we queue up edits and delay adding them to the undo stack until PostChanged.
+ ///
+ /// It would be good to revisit this at again, but we would need to work with VB
+ /// to fix their snippets / undo behavior, and verify that VB commit is also unaffected.
+ /// </remarks>
+ private void TextBufferPostChanged(object sender, EventArgs e)
+ {
+ // Only process a top level PostChanged event. Nested events will continue to process TextChange events
+ // which are added to the queue and will be processed below
+ if ( _inPostChanged )
+ {
+ return;
+ }
+
+ _inPostChanged = true;
+ try
+ {
+ // Do not do a foreach loop here. It's perfectly possible, and in fact expected, that the Complete
+ // method below can trigger a series of events which leads to a nested edit and another
+ // ITextBuffer::Changed. That event will add to the _editVersionList queue and hence break a
+ // foreach loop
+ while ( _editVersionList.Count > 0 )
+ {
+ var cur = _editVersionList.Dequeue();
+ using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged))
+ {
+ TextBufferChangeUndoPrimitive undoPrimitive = new TextBufferChangeUndoPrimitive(_undoHistory, cur);
+ undoTransaction.AddUndo(undoPrimitive);
+
+ undoTransaction.Complete();
+ }
+ }
+ }
+ finally
+ {
+ _editVersionList.Clear(); // Ensure we cleanup state in the face of an exception
+ _inPostChanged = false;
+ }
+ }
+
+ void TextBufferChanging(object sender, TextContentChangingEventArgs e)
+ {
+ // See if somebody (other than us) is trying to edit the buffer during undo/redo.
+ if (_undoHistory.State != TextUndoHistoryState.Idle &&
+ (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive))
+ {
+ Debug.Fail("Attempt to edit the buffer during undo/redo has been denied. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code.");
+ e.Cancel();
+ }
+ }
+
+
+ #endregion
+
+ #region ITextBufferUndoManager Members
+
+ public ITextBuffer TextBuffer
+ {
+ get { return _textBuffer; }
+ }
+
+ public ITextUndoHistory TextBufferUndoHistory
+ {
+ // Note, right now, there is no way for us to know if an ITextUndoHistory
+ // has been unregistered (ie it can be unregistered by a third party)
+ // An issue has been logged with the Undo team, but in the mean time, to ensure that
+ // we are robust, always register the undo history.
+ get
+ {
+ _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer);
+ return _undoHistory;
+ }
+ }
+
+ public void UnregisterUndoHistory()
+ {
+ // Unregister the undo history
+ _undoHistoryRegistry.RemoveHistory(_undoHistory);
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ _textBuffer.Changed -= TextBufferChanged;
+ _textBuffer.PostChanged -= TextBufferPostChanged;
+ _textBuffer.Changing -= TextBufferChanging;
+
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs
new file mode 100644
index 0000000..72eb736
--- /dev/null
+++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs
@@ -0,0 +1,75 @@
+//
+// 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.BufferUndoManager.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Operations;
+ using System.ComponentModel.Composition;
+
+ [Export(typeof(ITextBufferUndoManagerProvider))]
+ internal sealed class TextBufferUndoManagerProvider : ITextBufferUndoManagerProvider
+ {
+ [Import]
+ internal ITextUndoHistoryRegistry _undoHistoryRegistry { get; set; }
+
+ /// <summary>
+ /// Provides an <see cref="ITextBufferUndoManager"/> for the given <paramref name="textBuffer"/>.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="ITextBuffer"/> to create the <see cref="ITextBufferUndoManager"/> for.</param>
+ /// <returns>A cached <see cref="ITextBufferUndoManager"/> for the given <paramref name="textBuffer"/>.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="textBuffer" /> is null.</exception>
+ public ITextBufferUndoManager GetTextBufferUndoManager(ITextBuffer textBuffer)
+ {
+ // Validate
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ // See if there was already a TextBufferUndoManager created for the given textBuffer, we only ever want to create one
+ ITextBufferUndoManager cachedBufferUndoManager;
+ if (!textBuffer.Properties.TryGetProperty<ITextBufferUndoManager>(typeof(ITextBufferUndoManager), out cachedBufferUndoManager))
+ {
+ cachedBufferUndoManager = new TextBufferUndoManager(textBuffer, _undoHistoryRegistry);
+ textBuffer.Properties.AddProperty(typeof(ITextBufferUndoManager), cachedBufferUndoManager);
+ }
+
+ return cachedBufferUndoManager;
+ }
+
+ /// <summary>
+ /// If the specified <paramref name="textBuffer" /> has an <see cref="ITextBufferUndoManager" /> associated with it, remove it.
+ /// </summary>
+ /// <exception cref="ArgumentNullException"><paramref name="textBuffer" /> is null.</exception>
+ public void RemoveTextBufferUndoManager(ITextBuffer textBuffer)
+ {
+ // Validate
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ ITextBufferUndoManager cachedBufferUndoManager;
+ if (textBuffer.Properties.TryGetProperty<ITextBufferUndoManager>(typeof(ITextBufferUndoManager), out cachedBufferUndoManager))
+ {
+ // Dispose() so it stops listening to Changed events on the buffer.
+ IDisposable disposableBufferUndoManager = cachedBufferUndoManager as IDisposable;
+ if (disposableBufferUndoManager != null)
+ {
+ disposableBufferUndoManager.Dispose();
+ }
+
+ // Remove from cache.
+ textBuffer.Properties.RemoveProperty(typeof(ITextBufferUndoManager));
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/BaseBuffer.cs b/src/Text/Impl/TextModel/BaseBuffer.cs
new file mode 100644
index 0000000..801dfbf
--- /dev/null
+++ b/src/Text/Impl/TextModel/BaseBuffer.cs
@@ -0,0 +1,1120 @@
+//
+// 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.Collections.Generic;
+ using System.Diagnostics;
+ using System.Threading;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Threading;
+ using System.Threading.Tasks;
+
+ internal abstract partial class BaseBuffer : ITextBuffer2
+ {
+ #region ITextEventRaiser Interface
+ /// <summary>
+ /// Implemented internally to support a heterogeneous event queue.
+ /// </summary>
+ internal interface ITextEventRaiser
+ {
+ void RaiseEvent(BaseBuffer baseBuffer, bool immediate);
+ bool HasPostEvent { get; }
+ }
+ #endregion
+
+ #region TextContentChangedEventRaiser Class
+ /// <summary>
+ /// The agent that knows how to raise ordinary TextContent changed events.
+ /// </summary>
+ internal class TextContentChangedEventRaiser : ITextEventRaiser
+ {
+ private TextContentChangedEventArgs args;
+
+ public TextContentChangedEventRaiser(ITextSnapshot beforeSnapshot,
+ ITextSnapshot afterSnapshot,
+ EditOptions options,
+ Object editTag)
+ {
+ args = new TextContentChangedEventArgs(beforeSnapshot, afterSnapshot, options, editTag);
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ baseBuffer.RawRaiseEvent(args, immediate);
+ }
+
+ public bool HasPostEvent
+ {
+ get { return true; }
+ }
+ }
+ #endregion
+
+ #region TextBufferBaseEdit Class Definition
+ /// <summary>
+ /// Checking for edits already in progress and modifications on the proper thread.
+ /// </summary>
+ protected abstract class TextBufferBaseEdit : IDisposable
+ {
+ protected BaseBuffer baseBuffer;
+ protected bool applied;
+ protected bool canceled;
+
+ public TextBufferBaseEdit(BaseBuffer baseBuffer)
+ {
+ this.baseBuffer = baseBuffer;
+ if (!baseBuffer.CheckEditAccess())
+ {
+ throw new InvalidOperationException(Strings.InvalidTextBufferEditThread);
+ }
+ if (baseBuffer.editInProgress)
+ {
+ throw new InvalidOperationException(Strings.SimultaneousEdit);
+ }
+ baseBuffer.editInProgress = true;
+ baseBuffer.group.BeginEdit();
+ }
+
+ public virtual void Cancel()
+ {
+ this.CancelApplication();
+ }
+
+ public virtual void CancelApplication()
+ {
+ if (!this.canceled)
+ {
+ this.canceled = true;
+ this.baseBuffer.editInProgress = false;
+ this.baseBuffer.group.CancelEdit();
+ }
+ }
+
+ public bool Canceled
+ {
+ get
+ {
+ return this.canceled;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!this.applied && !this.canceled)
+ {
+ this.CancelApplication();
+ }
+ GC.SuppressFinalize(this);
+ }
+ }
+ #endregion
+
+ #region TextBufferEdit Class Definition
+ /// <summary>
+ /// Edit protocol checking.
+ /// </summary>
+ protected abstract partial class TextBufferEdit : TextBufferBaseEdit
+ {
+ protected ITextSnapshot originSnapshot;
+ protected object editTag;
+
+ public TextBufferEdit(BaseBuffer baseBuffer, ITextSnapshot snapshot, object editTag)
+ : base(baseBuffer)
+ {
+ this.baseBuffer = baseBuffer;
+ this.originSnapshot = snapshot;
+ this.editTag = editTag;
+ }
+
+ public ITextSnapshot Snapshot
+ {
+ get { return this.originSnapshot; }
+ }
+
+ public ITextSnapshot Apply()
+ {
+ ITextSnapshot snapshot;
+ try
+ {
+ snapshot = PerformApply();
+ }
+ finally
+ {
+ // TextBufferBaseEdit.Cancel may have been called via
+ // the cancellable Changing event. In that case, group.CancelEdit will
+ // have been called and canceled will be true.
+ if (!this.canceled)
+ {
+ this.baseBuffer.group.FinishEdit();
+ }
+ }
+
+ return snapshot;
+ }
+
+ protected abstract ITextSnapshot PerformApply();
+
+ protected void CheckActive()
+ {
+ if (this.canceled)
+ {
+ throw new InvalidOperationException(Strings.ContinueCanceledEdit);
+ }
+ if (this.applied)
+ {
+ throw new InvalidOperationException(Strings.ReuseAppliedEdit);
+ }
+ }
+ }
+ #endregion
+
+ #region Edit Class Definition
+ /// <summary>
+ /// Fundamental editing operations.
+ /// </summary>
+ protected abstract partial class Edit : TextBufferEdit, ITextEdit
+ {
+ private readonly int bufferLength;
+ protected FrugalList<TextChange> changes;
+ protected readonly EditOptions options;
+ protected readonly int? reiteratedVersionNumber;
+ private TextContentChangingEventArgs raisedChangingEventArgs;
+ private Action cancelAction;
+ private bool hasFailedChanges;
+
+ protected Edit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, Object editTag)
+ : base(baseBuffer, originSnapshot, editTag)
+ {
+ this.bufferLength = originSnapshot.Length;
+ this.changes = new FrugalList<TextChange>();
+ this.options = options;
+ this.reiteratedVersionNumber = reiteratedVersionNumber;
+ this.raisedChangingEventArgs = null;
+ this.cancelAction = null;
+ this.hasFailedChanges = false;
+ }
+
+ public bool Insert(int position, string text)
+ {
+ CheckActive();
+ if (position < 0 || position > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(position, isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (text.Length != 0)
+ {
+ this.changes.Add(TextChange.Create(position, string.Empty, text, this.originSnapshot));
+ }
+ return true;
+ }
+
+ public bool Insert(int position, char[] characterBuffer, int startIndex, int length)
+ {
+ CheckActive();
+ if (position < 0 || position > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (characterBuffer == null)
+ {
+ throw new ArgumentNullException("characterBuffer");
+ }
+ if (startIndex < 0 || startIndex > characterBuffer.Length)
+ {
+ throw new ArgumentOutOfRangeException("startIndex");
+ }
+ if (length < 0 || startIndex + length > characterBuffer.Length)
+ {
+ throw new ArgumentOutOfRangeException("length");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(position, isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (length != 0)
+ {
+ this.changes.Add(TextChange.Create(position, string.Empty, new string(characterBuffer, startIndex, length), this.originSnapshot));
+ }
+ return true;
+ }
+
+ public bool Replace(int startPosition, int charsToReplace, string replaceWith)
+ {
+ CheckActive();
+ if (startPosition < 0 || startPosition > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("startPosition");
+ }
+ if (charsToReplace < 0 || startPosition + charsToReplace > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("charsToReplace");
+ }
+ if (replaceWith == null)
+ {
+ throw new ArgumentNullException("replaceWith");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(new Span(startPosition, charsToReplace), isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (charsToReplace != 0 || replaceWith.Length != 0)
+ {
+ this.changes.Add(TextChange.Create(startPosition, DeletionChangeString(new Span(startPosition, charsToReplace)), replaceWith, this.originSnapshot));
+ }
+ return true;
+ }
+
+ public bool Replace(Span replaceSpan, string replaceWith)
+ {
+ CheckActive();
+ if (replaceSpan.End > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("replaceSpan");
+ }
+ if (replaceWith == null)
+ {
+ throw new ArgumentNullException("replaceWith");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(replaceSpan, isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (replaceSpan.Length != 0 || replaceWith.Length != 0)
+ {
+ this.changes.Add(TextChange.Create(replaceSpan.Start, DeletionChangeString(replaceSpan), replaceWith, this.originSnapshot));
+ }
+ return true;
+ }
+
+ public bool Delete(int startPosition, int charsToDelete)
+ {
+ CheckActive();
+ if (startPosition < 0 || startPosition > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("startPosition");
+ }
+ if (charsToDelete < 0 || startPosition + charsToDelete > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("charsToDelete");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(new Span(startPosition, charsToDelete), isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (charsToDelete != 0)
+ {
+ this.changes.Add(TextChange.Create(startPosition, DeletionChangeString(new Span(startPosition, charsToDelete)), StringRebuilder.Empty, this.originSnapshot));
+ }
+ return true;
+ }
+
+ public bool Delete(Span deleteSpan)
+ {
+ CheckActive();
+ if (deleteSpan.End > this.bufferLength)
+ {
+ throw new ArgumentOutOfRangeException("deleteSpan");
+ }
+
+ // Check for ReadOnly
+ if (this.baseBuffer.IsReadOnlyImplementation(deleteSpan, isEdit: true))
+ {
+ this.hasFailedChanges = true;
+ return false;
+ }
+
+ if (deleteSpan.Length != 0)
+ {
+ this.changes.Add(TextChange.Create(deleteSpan.Start, DeletionChangeString(deleteSpan), StringRebuilder.Empty, this.originSnapshot));
+ }
+ return true;
+ }
+
+ private StringRebuilder DeletionChangeString(Span deleteSpan)
+ {
+ return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(this.originSnapshot, deleteSpan);
+ }
+
+ /// <summary>
+ /// Checks whether the edit on the buffer is allowed to continue.
+ /// </summary>
+ /// <param name="cancelationResponse">Additional action to perform if the edit itself is canceled.</param>
+ public bool CheckForCancellation(Action cancelationResponse)
+ {
+ Debug.Assert(this.raisedChangingEventArgs == null, "just checking");
+
+ // If no changes are being applied to this edit's buffer then there will be no new snapshot produced and
+ // the Changed event won't be raised and so the cancelable Changing event should not be raised either.
+ if (this.changes.Count == 0)
+ {
+ return true;
+ }
+
+ if (this.raisedChangingEventArgs == null)
+ {
+ this.cancelAction = cancelationResponse;
+ this.raisedChangingEventArgs = new TextContentChangingEventArgs(this.Snapshot, this.editTag, (args) =>
+ {
+ this.Cancel();
+ });
+ this.baseBuffer.RaiseChangingEvent(this.raisedChangingEventArgs);
+ }
+ this.canceled = this.raisedChangingEventArgs.Canceled;
+ //Debug.Assert(!this.canceled || !this.applied, "an edit shouldn't be both canceled and applied");
+ return !this.raisedChangingEventArgs.Canceled;
+ }
+
+ public override void Cancel()
+ {
+ base.Cancel();
+
+ if (this.cancelAction != null)
+ {
+ this.cancelAction();
+ }
+ }
+
+ public bool HasEffectiveChanges
+ {
+ get
+ {
+ return this.changes.Count > 0;
+ }
+ }
+
+ public bool HasFailedChanges
+ {
+ get
+ {
+ return this.hasFailedChanges;
+ }
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder builder = new System.Text.StringBuilder();
+ for (int c = 0; c < this.changes.Count; ++c)
+ {
+ TextChange change = this.changes[c];
+ builder.Append(change.ToString(brief: true));
+ if (c < this.changes.Count - 1)
+ {
+ builder.Append("\r\n");
+ }
+ }
+ return builder.ToString();
+ }
+
+ public void RecordMasterChangeOffset(int masterChangeOffset)
+ {
+ if (this.changes.Count == 0)
+ {
+ throw new InvalidOperationException("Can't record a change offset without a change.");
+ }
+
+ this.changes[this.changes.Count - 1].RecordMasterChangeOffset(masterChangeOffset);
+ }
+ }
+ #endregion // Edit Class Definition
+
+ #region Read Only Region Edit Class Definition
+ private sealed partial class ReadOnlyRegionEdit : TextBufferEdit, IReadOnlyRegionEdit
+ {
+ private List<IReadOnlyRegion> readOnlyRegionsToAdd = new List<IReadOnlyRegion>();
+ private List<IReadOnlyRegion> readOnlyRegionsToRemove = new List<IReadOnlyRegion>();
+
+ private int aggregateEnd = int.MinValue;
+ private int aggregateStart = int.MaxValue;
+
+ public ReadOnlyRegionEdit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, Object editTag)
+ : base(baseBuffer, originSnapshot, editTag)
+ {
+ }
+
+ protected override ITextSnapshot PerformApply()
+ {
+ CheckActive();
+
+ this.applied = true;
+
+ if ((this.readOnlyRegionsToAdd.Count > 0) || (this.readOnlyRegionsToRemove.Count > 0))
+ {
+ if (this.readOnlyRegionsToAdd.Count > 0)
+ {
+ // We leave the read only regions collection on the buffer null
+ // since most buffers will never have a read only region. Since
+ // regions are being added, create it now.
+ if (this.baseBuffer.readOnlyRegions == null)
+ {
+ this.baseBuffer.readOnlyRegions = new FrugalList<IReadOnlyRegion>();
+ }
+
+ this.baseBuffer.readOnlyRegions.AddRange(this.readOnlyRegionsToAdd);
+ }
+
+ if (this.readOnlyRegionsToRemove.Count > 0)
+ {
+ // We've already verified that it makes sense to remove these read only
+ // regions, so just proceed without further checks.
+ foreach (IReadOnlyRegion readOnlyRegion in this.readOnlyRegionsToRemove)
+ {
+ this.baseBuffer.readOnlyRegions.Remove(readOnlyRegion);
+ }
+ }
+
+ // Save off the current state of the read only spans
+ this.baseBuffer.readOnlyRegionSpanCollection = new ReadOnlySpanCollection(this.baseBuffer.CurrentVersion, this.baseBuffer.readOnlyRegions);
+
+ ReadOnlyRegionsChangedEventRaiser raiser =
+ new ReadOnlyRegionsChangedEventRaiser(new SnapshotSpan(this.baseBuffer.CurrentSnapshot, this.aggregateStart, this.aggregateEnd - this.aggregateStart));
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ // no immediate event for read only regions
+ this.baseBuffer.editInProgress = false;
+ }
+ else
+ {
+ this.baseBuffer.editInProgress = false;
+ }
+
+ // no new snapshot
+ return this.originSnapshot;
+ }
+
+ #region IReadOnlyRegionEdit Members
+
+ public IReadOnlyRegion CreateReadOnlyRegion(Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode)
+ {
+ return CreateDynamicReadOnlyRegion(span, trackingMode, edgeInsertionMode, callback: null);
+ }
+
+ public IReadOnlyRegion CreateDynamicReadOnlyRegion(Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode, DynamicReadOnlyRegionQuery callback)
+ {
+ ReadOnlyRegion readOnlyRegion = new ReadOnlyRegion(this.baseBuffer.CurrentVersion, span, trackingMode, edgeInsertionMode, callback);
+
+ readOnlyRegionsToAdd.Add(readOnlyRegion);
+
+ this.aggregateStart = Math.Min(this.aggregateStart, span.Start);
+ this.aggregateEnd = Math.Max(this.aggregateEnd, span.End);
+
+ return readOnlyRegion;
+ }
+
+ public IReadOnlyRegion CreateReadOnlyRegion(Span span)
+ {
+ return CreateReadOnlyRegion(span, SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Allow);
+ }
+
+ public void RemoveReadOnlyRegion(IReadOnlyRegion readOnlyRegion)
+ {
+ // Throw if trying to remove a region if there aren't that many regions created.
+ if (this.baseBuffer.readOnlyRegions == null)
+ {
+ throw new InvalidOperationException(Strings.RemoveNoReadOnlyRegion);
+ }
+
+ // Throw if trying to remove a region from the wrong buffer
+ if (this.readOnlyRegionsToRemove.Exists(delegate(IReadOnlyRegion match) { return !object.ReferenceEquals(match.Span.TextBuffer, this.baseBuffer); }))
+ {
+ throw new InvalidOperationException(Strings.InvalidReadOnlyRegion);
+ }
+
+ this.readOnlyRegionsToRemove.Add(readOnlyRegion);
+
+ Span regionSpan = readOnlyRegion.Span.GetSpan(this.baseBuffer.CurrentSnapshot);
+ this.aggregateStart = Math.Min(this.aggregateStart, regionSpan.Start);
+ this.aggregateEnd = Math.Max(this.aggregateEnd, regionSpan.End);
+ }
+
+ #endregion
+ }
+ #endregion // Read Only Region Edit Class Definition
+
+ #region ContentType Edit Class Definition
+ private sealed class ContentTypeEdit : TextBufferEdit, ISubordinateTextEdit
+ {
+ private IContentType _newContentType;
+
+ public ContentTypeEdit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, Object editTag, IContentType newContentType)
+ : base(baseBuffer, originSnapshot, editTag)
+ {
+ _newContentType = newContentType;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.baseBuffer; }
+ }
+
+ protected override ITextSnapshot PerformApply()
+ {
+ CheckActive();
+
+ this.applied = true;
+
+ if (_newContentType != null)
+ {
+ // we need to perform a group edit because any projection buffers that use this buffer will
+ // generate new snapshots as independent edits.
+ this.baseBuffer.group.PerformMasterEdit(this.baseBuffer, this, EditOptions.None, this.editTag);
+ }
+ else
+ {
+ this.baseBuffer.editInProgress = false;
+ }
+
+ return this.baseBuffer.currentSnapshot;
+ }
+
+ public void PreApply()
+ {
+ // all the action is in FinalApply()
+ }
+
+ public void FinalApply()
+ {
+ IContentType beforeContentType = baseBuffer.contentType;
+ this.baseBuffer.contentType = _newContentType;
+ this.baseBuffer.SetCurrentVersionAndSnapshot(NormalizedTextChangeCollection.Empty);
+ ITextEventRaiser raiser = new ContentTypeChangedEventRaiser(this.originSnapshot, baseBuffer.currentSnapshot, beforeContentType, baseBuffer.contentType, editTag);
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ raiser.RaiseEvent(this.baseBuffer, true);
+ this.baseBuffer.editInProgress = false;
+ }
+
+ public bool CheckForCancellation(Action cancelAction)
+ {
+ // Not cancelable.
+ return true;
+ }
+
+ public void RecordMasterChangeOffset(int masterChangeOffset)
+ {
+ throw new InvalidOperationException("Content type edits shouldn't have change offsets.");
+ }
+ }
+ #endregion // IContentType Edit Class Definition
+
+ #region Private members and construction
+ private IContentType contentType;
+ private PropertyCollection properties;
+ private readonly Object syncLock = new Object();
+ private Thread editThread;
+ protected internal BufferGroup group;
+ protected internal StringRebuilder builder;
+ protected internal BaseSnapshot currentSnapshot;
+ protected TextVersion currentVersion;
+ private FrugalList<IReadOnlyRegion> readOnlyRegions;
+ protected ReadOnlySpanCollection readOnlyRegionSpanCollection;
+ protected internal bool editInProgress;
+ protected internal ITextDifferencingService textDifferencingService;
+ protected readonly GuardedOperations guardedOperations;
+
+ private static bool eventTracing = false;
+ private static int eventDepth = 0;
+
+ protected BaseBuffer(IContentType contentType, int initialLength, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations)
+ {
+ // parameters are validated outside
+ Debug.Assert(contentType != null);
+
+ this.contentType = contentType;
+ this.currentVersion = new TextVersion(this, new TextImageVersion(initialLength));
+ // this.builder should be set in calling ctor
+ this.textDifferencingService = textDifferencingService;
+ this.guardedOperations = guardedOperations;
+ }
+ #endregion
+
+ #region ITextBuffer members
+ public IContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public PropertyCollection Properties
+ {
+ get
+ {
+ if (this.properties == null)
+ {
+ lock (this.syncLock)
+ {
+ if (this.properties == null)
+ {
+ this.properties = new PropertyCollection();
+ }
+ }
+ }
+ return this.properties;
+ }
+ }
+
+ public ITextSnapshot CurrentSnapshot
+ {
+ get { return this.currentSnapshot; }
+ }
+
+ protected TextVersion CurrentVersion
+ {
+ get { return this.currentVersion; }
+ }
+
+ public abstract ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag);
+
+ public ITextEdit CreateEdit()
+ {
+ return CreateEdit(EditOptions.None, null, null);
+ }
+
+ public IReadOnlyRegionEdit CreateReadOnlyRegionEdit()
+ {
+ return CreateReadOnlyRegionEdit(null);
+ }
+
+ public IReadOnlyRegionEdit CreateReadOnlyRegionEdit(object editTag)
+ {
+ return new ReadOnlyRegionEdit(this, this.CurrentSnapshot, editTag);
+ }
+
+ public void ChangeContentType(IContentType newContentType, object editTag)
+ {
+ if (newContentType == null)
+ {
+ throw new ArgumentNullException("newContentType");
+ }
+
+ if (newContentType != this.contentType)
+ {
+ using (ContentTypeEdit edit = new ContentTypeEdit(this, this.currentSnapshot, editTag, newContentType))
+ {
+ edit.Apply();
+ }
+ }
+ }
+
+ public bool EditInProgress
+ {
+ get { return this.editInProgress; }
+ }
+
+ public void TakeThreadOwnership()
+ {
+ lock (this.syncLock)
+ {
+ if (this.editThread != null && this.editThread != Thread.CurrentThread)
+ {
+ throw new InvalidOperationException(Strings.InvalidBufferThreadOwnershipChange);
+ }
+ this.editThread = Thread.CurrentThread;
+ }
+ }
+
+ public bool CheckEditAccess()
+ {
+ return this.editThread == null || this.editThread == Thread.CurrentThread;
+ }
+
+ protected abstract BaseSnapshot TakeSnapshot();
+ #endregion
+
+ #region ReadOnlyRegion support
+ public bool IsReadOnly(int position)
+ {
+ return IsReadOnly(position, isEdit: false);
+ }
+
+ public bool IsReadOnly(int position, bool isEdit)
+ {
+ ReadOnlyQueryThreadCheck();
+ if ((position < 0) || (position > this.currentSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+
+ return IsReadOnlyImplementation(position, isEdit);
+ }
+
+ public bool IsReadOnly(Span span)
+ {
+ return IsReadOnly(span, isEdit: false);
+ }
+
+ public bool IsReadOnly(Span span, bool isEdit)
+ {
+ ReadOnlyQueryThreadCheck();
+ if (span.End > this.currentSnapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+
+ return IsReadOnlyImplementation(span, isEdit);
+ }
+
+ protected internal virtual bool IsReadOnlyImplementation(int position, bool isEdit)
+ {
+ if (this.readOnlyRegionSpanCollection == null)
+ {
+ return false;
+ }
+ return this.readOnlyRegionSpanCollection.IsReadOnly(position, this.currentSnapshot, isEdit);
+ }
+
+ protected internal virtual bool IsReadOnlyImplementation(Span span, bool isEdit)
+ {
+ if (this.readOnlyRegionSpanCollection == null)
+ {
+ return false;
+ }
+ return this.readOnlyRegionSpanCollection.IsReadOnly(span, this.currentSnapshot, isEdit);
+ }
+
+ public NormalizedSpanCollection GetReadOnlyExtents(Span span)
+ {
+ ReadOnlyQueryThreadCheck();
+ if (span.End > this.CurrentSnapshot.Length)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+ return GetReadOnlyExtentsImplementation(span);
+ }
+
+ protected internal virtual NormalizedSpanCollection GetReadOnlyExtentsImplementation(Span span)
+ {
+ FrugalList<Span> spans = new FrugalList<Span>();
+
+ if (this.readOnlyRegionSpanCollection != null)
+ {
+ foreach (ReadOnlySpan readOnlySpan in this.readOnlyRegionSpanCollection.QueryAllEffectiveReadOnlySpans(this.currentVersion))
+ {
+ Span readOnlySpanSpan = readOnlySpan.GetSpan(this.currentSnapshot);
+ Span? overlapSpan = (readOnlySpanSpan == span) ? readOnlySpanSpan : readOnlySpanSpan.Overlap(span);
+ if (overlapSpan.HasValue)
+ {
+ spans.Add(overlapSpan.Value);
+ }
+ }
+ }
+
+ return new NormalizedSpanCollection(spans);
+ }
+
+ private void ReadOnlyQueryThreadCheck()
+ {
+ if (!CheckEditAccess())
+ {
+ throw new InvalidOperationException(Strings.InvalidTextBufferEditThread);
+ }
+ }
+
+ public event EventHandler<SnapshotSpanEventArgs> ReadOnlyRegionsChanged;
+
+ protected class ReadOnlyRegionsChangedEventRaiser : ITextEventRaiser
+ {
+ private SnapshotSpan affectedSpan;
+
+ public ReadOnlyRegionsChangedEventRaiser(SnapshotSpan affectedSpan)
+ {
+ this.affectedSpan = affectedSpan;
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ // there is no immediate form of this event since it does not create a snapshot
+ Debug.Assert(!immediate);
+ EventHandler<SnapshotSpanEventArgs> handler = baseBuffer.ReadOnlyRegionsChanged;
+ if (handler != null)
+ {
+ var args = new SnapshotSpanEventArgs(affectedSpan);
+ baseBuffer.guardedOperations.RaiseEvent(baseBuffer, handler, args);
+ }
+ }
+
+ public bool HasPostEvent
+ {
+ get { return false; }
+ }
+ }
+ #endregion
+
+ #region IContentType change support
+ public event EventHandler<ContentTypeChangedEventArgs> ContentTypeChanged;
+
+ protected class ContentTypeChangedEventRaiser : ITextEventRaiser
+ {
+ #region Private Members
+ ITextSnapshot beforeSnapshot;
+ ITextSnapshot afterSnapshot;
+ object editTag;
+ IContentType beforeContentType;
+ IContentType afterContentType;
+ #endregion
+
+ public ContentTypeChangedEventRaiser(ITextSnapshot beforeSnapshot, ITextSnapshot afterSnapshot, IContentType beforeContentType, IContentType afterContentType, object editTag)
+ {
+ this.beforeSnapshot = beforeSnapshot;
+ this.afterSnapshot = afterSnapshot;
+ this.editTag = editTag;
+ this.beforeContentType = beforeContentType;
+ this.afterContentType = afterContentType;
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ EventHandler<ContentTypeChangedEventArgs> handler = immediate ? baseBuffer.ContentTypeChangedImmediate : baseBuffer.ContentTypeChanged;
+ if (handler != null)
+ {
+ var eventArgs = new ContentTypeChangedEventArgs(this.beforeSnapshot, this.afterSnapshot, this.beforeContentType, this.afterContentType, this.editTag);
+ baseBuffer.guardedOperations.RaiseEvent(baseBuffer, handler, eventArgs);
+ }
+ }
+
+ public bool HasPostEvent
+ {
+ get { return true; }
+ }
+ }
+ #endregion
+
+ #region Editing Shortcuts
+ public ITextSnapshot Insert(int position, string text)
+ {
+ using (ITextEdit textEdit = CreateEdit())
+ {
+ textEdit.Insert(position, text);
+ return textEdit.Apply();
+ }
+ }
+
+ public ITextSnapshot Delete(Span deleteSpan)
+ {
+ using (ITextEdit textEdit = CreateEdit())
+ {
+ textEdit.Delete(deleteSpan);
+ return textEdit.Apply();
+ }
+ }
+
+ public ITextSnapshot Replace(Span replaceSpan, string replaceWith)
+ {
+ using (ITextEdit textEdit = CreateEdit())
+ {
+ textEdit.Replace(replaceSpan, replaceWith);
+ return textEdit.Apply();
+ }
+ }
+ #endregion
+
+ #region Change Application and Eventing
+ protected void SetCurrentVersionAndSnapshot(INormalizedTextChangeCollection normalizedChanges, int reiteratedVersionNumber = -1)
+ {
+ this.currentVersion = this.currentVersion.CreateNext(normalizedChanges, newLength: -1, reiteratedVersionNumber: reiteratedVersionNumber);
+ this.builder = this.ApplyChangesToStringRebuilder(normalizedChanges, this.builder);
+ this.currentSnapshot = TakeSnapshot();
+ }
+
+ public StringRebuilder ApplyChangesToStringRebuilder(INormalizedTextChangeCollection normalizedChanges, StringRebuilder source)
+ {
+ var doppelganger = this.GetDoppelgangerBuilder();
+ if (doppelganger != null)
+ return doppelganger;
+
+ for (int i = normalizedChanges.Count - 1; (i >= 0); --i)
+ {
+ ITextChange change = normalizedChanges[i];
+ source = source.Replace(change.OldSpan, TextChange.NewStringRebuilder(change));
+ }
+
+ return source;
+ }
+
+ protected internal abstract ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag);
+ protected virtual StringRebuilder GetDoppelgangerBuilder() { return null; }
+
+ public event EventHandler<TextContentChangingEventArgs> Changing;
+
+ public event EventHandler<TextContentChangedEventArgs> ChangedHighPriority;
+ public event EventHandler<TextContentChangedEventArgs> Changed;
+ public event EventHandler<TextContentChangedEventArgs> ChangedLowPriority;
+
+ public event EventHandler PostChanged;
+ public event EventHandler<TextContentChangedEventArgs> ChangedOnBackground;
+ private Task _lastChangeOnBackgroundRaisedEvent = TextUtilities.CompletedNonInliningTask;
+
+ internal event EventHandler<TextContentChangedEventArgs> ChangedImmediate;
+ internal event EventHandler<ContentTypeChangedEventArgs> ContentTypeChangedImmediate;
+
+ internal void RawRaiseEvent(TextContentChangedEventArgs args, bool immediate)
+ {
+ if (immediate)
+ {
+ EventHandler<TextContentChangedEventArgs> immediateHandler = ChangedImmediate;
+ if (immediateHandler != null)
+ {
+ if (BaseBuffer.eventTracing)
+ {
+ Debug.WriteLine("<<< Imm events from " + ToString());
+ }
+ immediateHandler(this, args);
+ }
+ return;
+ }
+
+ EventHandler<TextContentChangedEventArgs> highHandler = ChangedHighPriority;
+ EventHandler<TextContentChangedEventArgs> medHandler = Changed;
+ EventHandler<TextContentChangedEventArgs> lowHandler = ChangedLowPriority;
+ EventHandler<TextContentChangedEventArgs> changedOnBackgroundHandler = ChangedOnBackground;
+
+ BaseBuffer.eventDepth++;
+ string indent = BaseBuffer.eventTracing ? new String(' ', 3 * (BaseBuffer.eventDepth - 1)) : null;
+ if (highHandler != null)
+ {
+ if (BaseBuffer.eventTracing)
+ {
+ Debug.WriteLine(">>> " + indent + "High events from " + ToString());
+ }
+ this.guardedOperations.RaiseEvent(this, highHandler, args);
+ }
+ if (changedOnBackgroundHandler != null)
+ {
+ if (BaseBuffer.eventTracing)
+ {
+ Debug.WriteLine(">>> " + indent + "background events from " + ToString());
+ }
+
+ // As this is a background event, we need to make sure handlers are executed synchronized
+ // and in the order the edits were applied.
+ // TODO: with this implementation any handler might delay all subsequent handlers.
+ // That's true for other Changed* events too, but this event is raised on a background thread
+ // so introducing delays in a handler won't be that easily noticable, also being on a
+ // background thread might suggest it's actually ok to perform some long running
+ // calculation directly in the handler.
+ // For isolation purposes we need a chain of tasks per handler, or some other, more
+ // optimized isolation strategy. Tracked by #449694.
+ _lastChangeOnBackgroundRaisedEvent = _lastChangeOnBackgroundRaisedEvent
+ .ContinueWith(_ =>
+ {
+ // changedOnBackgroundHandler might be stale at this point, get the latest list of handlers
+ var currentChangedOnBackgroundHandler = ChangedOnBackground;
+ if (currentChangedOnBackgroundHandler != null)
+ {
+ this.guardedOperations.RaiseEvent(this, currentChangedOnBackgroundHandler, args);
+ }
+ },
+ CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
+ // Now register pending task with task tracker to ensure it's completed when editor host is shutdown
+ this.guardedOperations.NonJoinableTaskTracker?.Register(_lastChangeOnBackgroundRaisedEvent);
+ }
+ if (medHandler != null)
+ {
+ if (BaseBuffer.eventTracing)
+ {
+ Debug.WriteLine(">>> " + indent + "Med events from " + ToString());
+ }
+ this.guardedOperations.RaiseEvent(this, medHandler, args);
+ }
+ if (lowHandler != null)
+ {
+ if (BaseBuffer.eventTracing)
+ {
+ Debug.WriteLine(">>> " + indent + "Low events from " + ToString());
+ }
+ this.guardedOperations.RaiseEvent(this, lowHandler, args);
+ }
+ BaseBuffer.eventDepth--;
+ }
+
+ internal void RaisePostChangedEvent()
+ {
+ this.guardedOperations.RaiseEvent(this, PostChanged);
+ }
+
+ internal void RaiseChangingEvent(TextContentChangingEventArgs args)
+ {
+ var changing = this.Changing;
+
+ if (changing != null)
+ {
+ foreach (Delegate handlerDelegate in changing.GetInvocationList())
+ {
+ var handler = (EventHandler<TextContentChangingEventArgs>)handlerDelegate;
+ try
+ {
+ handler(this, args);
+ }
+ catch (Exception e)
+ {
+ this.guardedOperations.HandleException(handler, e);
+ }
+
+ if (args.Canceled)
+ return;
+ }
+ }
+ }
+ #endregion
+
+ #region Diagnostic Support
+
+ public override string ToString()
+ {
+ string suffix = TextUtilities.GetTag(this);
+ if (string.IsNullOrEmpty(suffix))
+ {
+ ITextSnapshot snap = this.currentSnapshot;
+ if (snap != null)
+ {
+ suffix = "\"" + snap.GetText(0, Math.Min(16, snap.Length)) + "\"";
+ }
+ }
+ return this.ContentType.TypeName + ":" + suffix;
+ }
+
+#if _DEBUG
+ private string DebugOnly_AllText
+ {
+ get
+ {
+ return this.currentSnapshot.DebugOnly_AllText;
+ }
+ }
+#endif
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/BaseSnapshot.cs b/src/Text/Impl/TextModel/BaseSnapshot.cs
new file mode 100644
index 0000000..db723d3
--- /dev/null
+++ b/src/Text/Impl/TextModel/BaseSnapshot.cs
@@ -0,0 +1,203 @@
+//
+// 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.Collections.Generic;
+ using System.IO;
+ using System.Text;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Base class for all varieties of Text Snapshots.
+ /// </summary>
+ internal abstract class BaseSnapshot : ITextSnapshot, ITextSnapshot2
+ {
+ #region State and Construction
+ protected readonly ITextVersion2 version;
+ private readonly IContentType contentType;
+
+ internal readonly StringRebuilder Content;
+ internal readonly ITextImage cachingContent;
+
+ protected BaseSnapshot(ITextVersion2 version, StringRebuilder content)
+ {
+ this.version = version;
+ this.Content = content;
+ this.cachingContent = CachingTextImage.Create(this.Content, version.ImageVersion);
+
+ // we must extract the content type here, because the content type of the text buffer may change later.
+ this.contentType = version.TextBuffer.ContentType;
+ }
+ #endregion
+
+ #region ITextSnapshot implementations
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.TextBufferHelper; }
+ }
+
+ public IContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public ITextVersion Version
+ {
+ get { return this.version; }
+ }
+
+ public string GetText(int startIndex, int length)
+ {
+ return GetText(new Span(startIndex, length));
+ }
+
+ public string GetText()
+ {
+ return GetText(new Span(0, this.Length));
+ }
+
+ #region Point and Span factories
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
+ {
+ return this.version.CreateTrackingPoint(position, trackingMode);
+ }
+
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ return this.version.CreateTrackingPoint(position, trackingMode, trackingFidelity);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode)
+ {
+ return this.version.CreateTrackingSpan(start, length, trackingMode);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ return this.version.CreateTrackingSpan(start, length, trackingMode, trackingFidelity);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode)
+ {
+ return this.version.CreateTrackingSpan(span, trackingMode, TrackingFidelityMode.Forward);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ return this.version.CreateTrackingSpan(span, trackingMode, trackingFidelity);
+ }
+ #endregion
+ #endregion
+
+ #region ITextSnapshot2 implementations
+ public void SaveToFile(string filePath, bool replaceFile, Encoding encoding)
+ {
+ FileUtilities.SaveSnapshot(this, replaceFile ? FileMode.Create : FileMode.CreateNew, encoding, filePath);
+ }
+ #endregion
+
+ #region ITextSnapshot abstract methods
+ protected abstract ITextBuffer TextBufferHelper { get; }
+
+ public int Length
+ {
+ get { return this.cachingContent.Length; }
+ }
+
+ public int LineCount
+ {
+ get { return this.cachingContent.LineCount; }
+ }
+
+ public string GetText(Span span)
+ {
+ return this.cachingContent.GetText(span);
+ }
+
+ public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ this.cachingContent.CopyTo(sourceIndex, destination, destinationIndex, count);
+ }
+
+ public char[] ToCharArray(int startIndex, int length)
+ {
+ return this.cachingContent.ToCharArray(startIndex, length);
+ }
+
+ public char this[int position]
+ {
+ get
+ {
+ return this.cachingContent[position];
+ }
+ }
+
+ public ITextSnapshotLine GetLineFromLineNumber(int lineNumber)
+ {
+ TextImageLine lineSpan = this.cachingContent.GetLineFromLineNumber(lineNumber);
+
+ return new TextSnapshotLine(this, lineSpan);
+ }
+
+ public ITextSnapshotLine GetLineFromPosition(int position)
+ {
+ int lineNumber = this.cachingContent.GetLineNumberFromPosition(position);
+ return this.GetLineFromLineNumber(lineNumber);
+ }
+
+ public int GetLineNumberFromPosition(int position)
+ {
+ return this.cachingContent.GetLineNumberFromPosition(position);
+ }
+
+ public IEnumerable<ITextSnapshotLine> Lines
+ {
+ get
+ {
+ // this is a naive implementation
+ int lineCount = this.cachingContent.LineCount;
+ for (int line = 0; line < lineCount; ++line)
+ {
+ yield return GetLineFromLineNumber(line);
+ }
+ }
+ }
+
+ public void Write(System.IO.TextWriter writer)
+ {
+ this.cachingContent.Write(writer, new Span(0, this.cachingContent.Length));
+ }
+
+ public void Write(System.IO.TextWriter writer, Span span)
+ {
+ this.cachingContent.Write(writer, span);
+ }
+ #endregion
+
+ public ITextImage TextImage => this.cachingContent;
+
+ public override string ToString()
+ {
+ return String.Format("version: {0} lines: {1} length: {2} \r\n content: {3}",
+ Version.VersionNumber, LineCount, Length,
+ Microsoft.VisualStudio.Text.Utilities.TextUtilities.Escape(this.GetText(0, Math.Min(40, this.Length))));
+ }
+
+#if _DEBUG
+ internal string DebugOnly_AllText
+ {
+ get
+ {
+ return this.GetText(0, Math.Min(this.Length, 1024*1024));
+ }
+ }
+#endif
+ }
+}
diff --git a/src/Text/Impl/TextModel/BufferFactoryService.cs b/src/Text/Impl/TextModel/BufferFactoryService.cs
new file mode 100644
index 0000000..975cbf0
--- /dev/null
+++ b/src/Text/Impl/TextModel/BufferFactoryService.cs
@@ -0,0 +1,409 @@
+//
+// 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.Collections;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.IO;
+ using System.IO.MemoryMappedFiles;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Projection.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Factory for TextBuffers and ProjectionBuffers.
+ /// </summary>
+ [Export(typeof(ITextImageFactoryService))]
+ [Export(typeof(ITextImageFactoryService2))]
+ [Export(typeof(ITextBufferFactoryService))]
+ [Export(typeof(ITextBufferFactoryService2))]
+ [Export(typeof(ITextBufferFactoryService3))]
+ [Export(typeof(IProjectionBufferFactoryService))]
+ internal partial class BufferFactoryService : ITextBufferFactoryService2, ITextBufferFactoryService3, IProjectionBufferFactoryService, IInternalTextBufferFactory, ITextImageFactoryService2
+ {
+ #region Standard Content Type Definitions
+ [Export]
+ [Name("any")]
+ public ContentTypeDefinition anyContentTypeDefinition;
+
+ [Export]
+ [Name("text")]
+ [BaseDefinition("any")]
+ public ContentTypeDefinition textContentTypeDefinition;
+
+ [Export]
+ [Name("projection")]
+ [BaseDefinition("any")]
+ public ContentTypeDefinition projectionContentTypeDefinition;
+
+ [Export]
+ [Name("plaintext")]
+ [BaseDefinition("text")]
+ public ContentTypeDefinition plaintextContentTypeDefinition;
+
+ [Export]
+ [Name("code")]
+ [BaseDefinition("text")]
+ public ContentTypeDefinition codeContentType;
+
+ [Export]
+ [Name("inert")]
+ // N.B.: This ContentType does NOT inherit from anything
+ public ContentTypeDefinition inertContentTypeDefinition;
+ #endregion
+
+ #region Service Consumptions
+
+ [Import]
+ internal IContentTypeRegistryService _contentTypeRegistryService { get; set; }
+
+ [Import]
+ internal IDifferenceService _differenceService { get; set; }
+
+ [Import]
+ internal ITextDifferencingSelectorService _textDifferencingSelectorService { get; set; }
+
+ [Import]
+ internal GuardedOperations _guardedOperations { get; set; }
+
+ #endregion
+
+ #region Private state
+ private IContentType textContentType;
+ private IContentType plaintextContentType;
+ private IContentType inertContentType;
+ private IContentType projectionContentType;
+ #endregion
+
+ #region ContentType accessors
+ public IContentType TextContentType
+ {
+ get
+ {
+ if (this.textContentType == null)
+ {
+ // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock
+ this.textContentType = _contentTypeRegistryService.GetContentType("text");
+ }
+ return this.textContentType;
+ }
+ }
+
+ public IContentType PlaintextContentType
+ {
+ get
+ {
+ if (this.plaintextContentType == null)
+ {
+ // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock
+ this.plaintextContentType = _contentTypeRegistryService.GetContentType("plaintext");
+ }
+ return this.plaintextContentType;
+ }
+ }
+
+ public IContentType InertContentType
+ {
+ get
+ {
+ if (this.inertContentType == null)
+ {
+ // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock
+ this.inertContentType = _contentTypeRegistryService.GetContentType("inert");
+ }
+ return this.inertContentType;
+ }
+ }
+
+ public IContentType ProjectionContentType
+ {
+ get
+ {
+ if (this.projectionContentType == null)
+ {
+ // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock
+ this.projectionContentType = _contentTypeRegistryService.GetContentType("projection");
+ }
+ return this.projectionContentType;
+ }
+ }
+ #endregion
+
+ public ITextBuffer CreateTextBuffer()
+ {
+ return Make(TextContentType, StringRebuilder.Empty, false);
+ }
+
+ public ITextBuffer CreateTextBuffer(IContentType contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ return Make(contentType, StringRebuilder.Empty, false);
+ }
+
+ public ITextBuffer CreateTextBuffer(string text, IContentType contentType)
+ {
+ return CreateTextBuffer(text, contentType, false);
+ }
+
+ public ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ StringRebuilder content = StringRebuilderFromSnapshotSpan(span);
+
+ return Make(contentType, content, false);
+ }
+
+ public ITextBuffer CreateTextBuffer(ITextImage image, IContentType contentType)
+ {
+ if (image == null)
+ {
+ throw new ArgumentNullException(nameof(image));
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ StringRebuilder content = StringRebuilder.Create(image);
+
+ return Make(contentType, content, false);
+ }
+
+ public ITextBuffer CreateTextBuffer(string text, IContentType contentType, bool spurnGroup)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ return Make(contentType, StringRebuilder.Create(text), spurnGroup);
+ }
+
+ public ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length, string traceId)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ if (length > int.MaxValue)
+ {
+ throw new InvalidOperationException(Strings.FileTooLarge);
+ }
+
+ bool hasConsistentLineEndings;
+ int longestLineLength;
+ StringRebuilder content = TextImageLoader.Load(reader, length, traceId, out hasConsistentLineEndings, out longestLineLength);
+
+ ITextBuffer buffer = Make(contentType, content, false);
+ 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
+ buffer.Properties.AddProperty("InconsistentLineEndings", true);
+ }
+ // leave a similar sign about the longest line in the buffer.
+
+ return buffer;
+ }
+
+ public ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType)
+ {
+ return CreateTextBuffer(reader, contentType, -1, "legacy");
+ }
+
+ internal static StringRebuilder StringRebuilderFromSnapshotAndSpan(ITextSnapshot snapshot, Span span)
+ {
+ return AppendStringRebuildersFromSnapshotAndSpan(StringRebuilder.Empty, snapshot, span);
+ }
+
+ internal static StringRebuilder StringRebuilderFromSnapshotSpan(SnapshotSpan span)
+ {
+ return StringRebuilderFromSnapshotAndSpan(span.Snapshot, span.Span);
+ }
+
+ internal static StringRebuilder StringRebuilderFromSnapshotSpans(IList<SnapshotSpan> sourceSpans, Span selectedSourceSpans)
+ {
+ StringRebuilder content = StringRebuilder.Empty;
+ for (int i = 0; (i < selectedSourceSpans.Length); ++i)
+ {
+ var span = sourceSpans[selectedSourceSpans.Start + i];
+ content = AppendStringRebuildersFromSnapshotAndSpan(content, span.Snapshot, span.Span);
+ }
+
+ return content;
+ }
+
+ internal static StringRebuilder AppendStringRebuildersFromSnapshotAndSpan(StringRebuilder content, ITextSnapshot snapshot, Span span)
+ {
+ var baseSnapshot = snapshot as BaseSnapshot;
+ if (baseSnapshot != null)
+ {
+ content = content.Append(baseSnapshot.Content.GetSubText(span));
+ }
+ else
+ {
+ // The we don't know what to do fallback. This should never be called unless someone provides a new snapshot
+ // implementation.
+ content = content.Append(snapshot.GetText(span));
+ }
+
+ return content;
+ }
+
+ #region ITextImageFactoryService members
+ public ITextImage CreateTextImage(string text)
+ {
+ return CachingTextImage.Create(StringRebuilder.Create(text), null);
+ }
+
+ public ITextImage CreateTextImage(TextReader reader, long length)
+ {
+ bool hasConsistentLineEndings;
+ int longestLineLength;
+
+ return CachingTextImage.Create(TextImageLoader.Load(reader, length, string.Empty, out hasConsistentLineEndings, out longestLineLength), null);
+ }
+
+ public ITextImage CreateTextImage(MemoryMappedFile source)
+ {
+ // Evil implementation (for now) that just reads the entire contents of the MMF.
+ // Eventually to be replaced with something along the lines of a version of the StringRebuilderForCompressedChars that uses the MMF directly.
+ using (var stream = source.CreateViewStream())
+ {
+ using (var reader = new StreamReader(stream, Encoding.Unicode))
+ {
+ return this.CreateTextImage(reader, -1);
+ }
+ }
+ }
+ #endregion
+
+ private TextBuffer Make(IContentType contentType, StringRebuilder content, bool spurnGroup)
+ {
+ TextBuffer buffer = new TextBuffer(contentType, content, _textDifferencingSelectorService.DefaultTextDifferencingService, _guardedOperations, spurnGroup);
+ RaiseTextBufferCreatedEvent(buffer);
+ return buffer;
+ }
+
+ public IProjectionBuffer CreateProjectionBuffer(IProjectionEditResolver projectionEditResolver,
+ IList<object> trackingSpans,
+ ProjectionBufferOptions options,
+ IContentType contentType)
+ {
+ // projectionEditResolver is allowed to be null.
+ if (trackingSpans == null)
+ {
+ throw new ArgumentNullException("trackingSpans");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ IProjectionBuffer buffer =
+ new ProjectionBuffer(this, projectionEditResolver, contentType, trackingSpans, _differenceService, _textDifferencingSelectorService.DefaultTextDifferencingService, options, _guardedOperations);
+ RaiseProjectionBufferCreatedEvent(buffer);
+ return buffer;
+ }
+
+ public IProjectionBuffer CreateProjectionBuffer(IProjectionEditResolver projectionEditResolver,
+ IList<object> trackingSpans,
+ ProjectionBufferOptions options)
+ {
+ // projectionEditResolver is allowed to be null.
+ if (trackingSpans == null)
+ {
+ throw new ArgumentNullException("trackingSpans");
+ }
+
+ IProjectionBuffer buffer =
+ new ProjectionBuffer(this, projectionEditResolver, ProjectionContentType, trackingSpans, _differenceService, _textDifferencingSelectorService.DefaultTextDifferencingService, options, _guardedOperations);
+ RaiseProjectionBufferCreatedEvent(buffer);
+ return buffer;
+ }
+
+ public IElisionBuffer CreateElisionBuffer(IProjectionEditResolver projectionEditResolver,
+ NormalizedSnapshotSpanCollection exposedSpans,
+ ElisionBufferOptions options,
+ IContentType contentType)
+ {
+ // projectionEditResolver is allowed to be null.
+ if (exposedSpans == null)
+ {
+ throw new ArgumentNullException("exposedSpans");
+ }
+ if (exposedSpans.Count == 0)
+ {
+ throw new ArgumentOutOfRangeException("exposedSpans"); // really?
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ if (exposedSpans[0].Snapshot != exposedSpans[0].Snapshot.TextBuffer.CurrentSnapshot)
+ {
+ // TODO:
+ // build against given snapshot and then move forward if necessary?
+ throw new ArgumentException("Elision buffer must be created against the current snapshot of its source buffer");
+ }
+
+ IElisionBuffer buffer = new ElisionBuffer(projectionEditResolver, contentType, exposedSpans[0].Snapshot.TextBuffer,
+ exposedSpans, options, _textDifferencingSelectorService.DefaultTextDifferencingService, _guardedOperations);
+ RaiseProjectionBufferCreatedEvent(buffer);
+ return buffer;
+ }
+
+ public IElisionBuffer CreateElisionBuffer(IProjectionEditResolver projectionEditResolver,
+ NormalizedSnapshotSpanCollection exposedSpans,
+ ElisionBufferOptions options)
+ {
+ return CreateElisionBuffer(projectionEditResolver, exposedSpans, options, ProjectionContentType);
+ }
+
+ public event EventHandler<TextBufferCreatedEventArgs> TextBufferCreated;
+ public event EventHandler<TextBufferCreatedEventArgs> ProjectionBufferCreated;
+
+ private void RaiseTextBufferCreatedEvent(ITextBuffer buffer)
+ {
+ EventHandler<TextBufferCreatedEventArgs> handler = TextBufferCreated;
+ if (handler != null)
+ {
+ handler(this, new TextBufferCreatedEventArgs(buffer));
+ }
+ }
+
+ private void RaiseProjectionBufferCreatedEvent(IProjectionBufferBase buffer)
+ {
+ EventHandler<TextBufferCreatedEventArgs> handler = ProjectionBufferCreated;
+ if (handler != null)
+ {
+ handler(this, new TextBufferCreatedEventArgs(buffer));
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/BufferGroup.cs b/src/Text/Impl/TextModel/BufferGroup.cs
new file mode 100644
index 0000000..789b3de
--- /dev/null
+++ b/src/Text/Impl/TextModel/BufferGroup.cs
@@ -0,0 +1,688 @@
+//
+// 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.Collections.Generic;
+ using System.Diagnostics;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Projection.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+ using System;
+
+ internal class BufferGroup
+ {
+ private static bool tracing = false;
+ private static bool detailedTracing = false;
+ private static Exception LastMasterEditException = null;
+ private static string LastMasterEditExceptionStackTrace = null;
+
+ /// <summary>
+ /// The text buffers that are members of this group. All buffers related by projection must be in the
+ /// same group.
+ /// </summary>
+ private readonly HashSet<BufferWeakReference> members = new HashSet<BufferWeakReference>();
+
+ /// <summary>
+ /// This queue is used to sequence all events raised due to snapshot changes. It ensures that
+ /// events are always delivered in temporal order (i.e., no client is notified of snapshot N+1 before
+ /// every client is notified of snapshot N) and also to ensure that events related to a projection edit
+ /// are delivered 'bottom up'
+ /// </summary>
+ internal Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>> eventQueue = new Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>>();
+ internal int depth = 0;
+ internal bool eventingInProgress = false;
+
+ private class GraphEntry
+ {
+ public HashSet<IProjectionBufferBase> Targets { get; private set; }
+ public bool EditComplete { get; set; }
+ public bool Dependent { get; set; }
+ public GraphEntry(HashSet<IProjectionBufferBase> targets, bool editComplete, bool dependent)
+ {
+ Targets = targets;
+ EditComplete = editComplete;
+ Dependent = dependent;
+ }
+ }
+
+ /// <summary>
+ /// Transient fields that are valid during a 'master edit transaction'
+ /// </summary>
+ private BaseBuffer masterBuffer;
+ private Dictionary<BaseBuffer, GraphEntry> graph; // map from nodes to edges
+ private Dictionary<ITextBuffer, ISubordinateTextEdit> buffer2EditMap;
+ private HashSet<BaseProjectionBuffer> pendingIndependentBuffers;
+ private EditOptions masterOptions;
+ private object masterEditTag;
+
+ public BufferGroup(ITextBuffer member)
+ {
+ this.members.Add(new BufferWeakReference(member));
+ }
+
+ #region Membership
+ internal HashSet<BufferWeakReference> Members
+ {
+ get { return this.members; }
+ }
+
+ public bool MembersContains(ITextBuffer buffer)
+ {
+ return this.members.Contains(new BufferWeakReference(buffer));
+ }
+
+ public void AddMember(ITextBuffer member)
+ {
+ // Police this.members by removing the expired ones
+ this.members.RemoveWhere((m) => m.Buffer == null);
+
+ this.members.Add(new BufferWeakReference(member));
+ }
+
+ public void RemoveMember(ITextBuffer member)
+ {
+ // this is a very limited workaround to solve a Dev10 leak. New design for Dev11...
+ Debug.Assert(this.masterBuffer == null);
+ this.members.Remove(new BufferWeakReference(member));
+ }
+
+ public void Swallow(BufferGroup victim)
+ {
+ if (victim != this)
+ {
+ // todo: suitable checks to be sure it's OK to do this now
+ foreach (var memberWeakReference in victim.Members)
+ {
+ ITextBuffer member = memberWeakReference.Buffer;
+ if ((member != null) && this.members.Add(memberWeakReference))
+ {
+ BaseBuffer baseMember = (BaseBuffer)member;
+ baseMember.group = this;
+ }
+ }
+
+ if (victim.eventQueue.Count > 0)
+ {
+ // maybe reconsider using Queue type
+ var newEventQueue = new Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>>(victim.eventQueue);
+ while (this.eventQueue.Count > 0)
+ {
+ newEventQueue.Enqueue(this.eventQueue.Dequeue());
+ }
+ this.eventQueue = newEventQueue;
+ victim.eventQueue.Clear();
+ }
+ }
+ }
+ #endregion
+
+ public ITextBuffer MasterBuffer
+ {
+ get { return this.masterBuffer; }
+ }
+
+ public bool MasterEditInProgress
+ {
+ get { return this.masterBuffer != null; }
+ }
+
+ public static bool Tracing
+ {
+ get { return BufferGroup.tracing; }
+ set { BufferGroup.tracing = value; }
+ }
+
+ public static bool DetailedTracing
+ {
+ get { return BufferGroup.detailedTracing; }
+ set { BufferGroup.detailedTracing = value; }
+ }
+
+ public Dictionary<ITextBuffer, ISubordinateTextEdit> BufferToEditMap
+ {
+ get
+ {
+ if (this.buffer2EditMap == null)
+ {
+ throw new InvalidOperationException();
+ }
+ return this.buffer2EditMap;
+ }
+ }
+
+ public ITextEdit GetEdit(BaseBuffer buffer)
+ {
+ return GetEdit(buffer, this.masterOptions);
+ }
+
+ public ITextEdit GetEdit(BaseBuffer buffer, EditOptions options)
+ {
+ ISubordinateTextEdit subedit;
+ if (!this.buffer2EditMap.TryGetValue(buffer, out subedit))
+ {
+ Debug.Assert(!(buffer is IProjectionBufferBase));
+ subedit = buffer.CreateSubordinateEdit(options, null, this.masterEditTag);
+ this.buffer2EditMap.Add(buffer, subedit);
+ }
+ return (ITextEdit)subedit;
+ }
+
+ [Conditional("DEBUG")]
+ private static void Trace(ITextBuffer buffer, string operation)
+ {
+ if (BufferGroup.tracing)
+ {
+ string tag = TextUtilities.GetTag(buffer);
+ if (string.IsNullOrEmpty(tag))
+ {
+ tag = buffer.ToString();
+ }
+
+ Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "====> {0,12}: {1}", operation, tag));
+ }
+ }
+
+ [Conditional("DEBUG")]
+ private static void Trace(ISubordinateTextEdit subedit, string operation)
+ {
+ if (BufferGroup.tracing)
+ {
+ Trace(subedit.TextBuffer, operation);
+ Debug.WriteLine(subedit.ToString());
+ }
+ }
+
+ public void PerformMasterEdit(ITextBuffer buffer, ISubordinateTextEdit xedit, EditOptions options, object editTag)
+ {
+ Debug.Assert(this.MembersContains(buffer));
+ if (this.masterBuffer != null)
+ {
+ // internal error
+ throw new InvalidOperationException("Master edit already in progress");
+ }
+
+ try
+ {
+ this.masterBuffer = (BaseBuffer)buffer;
+ this.masterOptions = options;
+ this.masterEditTag = editTag;
+
+ this.buffer2EditMap = new Dictionary<ITextBuffer, ISubordinateTextEdit>();
+ this.buffer2EditMap.Add(buffer, xedit);
+
+ this.pendingIndependentBuffers = new HashSet<BaseProjectionBuffer>();
+
+ BuildGraph();
+
+ Trace(buffer, "Master");
+
+ Stack<ISubordinateTextEdit> appliedSubordinateEdits = new Stack<ISubordinateTextEdit>();
+ while (this.buffer2EditMap.Count > 0)
+ {
+ // Pick an edit that has no possibility of further impact from target projection buffers.
+ ISubordinateTextEdit subedit = PickEdit();
+ Trace(subedit, "Pre Apply");
+ PopulateSourceEdits(subedit.TextBuffer);
+ subedit.PreApply(); // this may add more edits to buffer2EditMap
+ appliedSubordinateEdits.Push(subedit);
+ }
+
+ Action cancelAction = () =>
+ {
+ foreach (var edit in appliedSubordinateEdits)
+ {
+ edit.CancelApplication();
+ }
+
+ Debug.Assert(this.pendingIndependentBuffers.Count == 0);
+ Debug.Assert(this.depth == 0);
+
+ this.graph = null;
+ this.buffer2EditMap = null;
+ this.masterBuffer = null;
+ this.pendingIndependentBuffers = null;
+ };
+
+ foreach (var edit in appliedSubordinateEdits)
+ {
+ if (!edit.CheckForCancellation(cancelAction))
+ {
+ Debug.Assert(this.graph == null);
+ Debug.Assert(this.buffer2EditMap == null);
+ Debug.Assert(this.masterBuffer == null);
+ Debug.Assert(this.pendingIndependentBuffers == null);
+ Debug.Assert(this.eventQueue.Count == 0);
+ Debug.Assert(this.depth == 0);
+ Debug.Assert(!this.eventingInProgress);
+ return;
+ }
+ }
+
+ // pendingIndependentBuffers currently do not get a voice in cancelation.
+
+ // now interpret events in the reverse order
+ while (appliedSubordinateEdits.Count > 0)
+ {
+ ISubordinateTextEdit subedit = appliedSubordinateEdits.Pop();
+ Trace(subedit.TextBuffer, "Final Apply");
+ subedit.FinalApply(); // this creates the snapshot and queues/raises events...TODO: make it return raisers
+ }
+
+ // move on to independent edits
+ while (this.pendingIndependentBuffers.Count > 0)
+ {
+ BaseProjectionBuffer projectionBuffer = PickIndependentBuffer();
+ Trace(projectionBuffer, "Propagate");
+ BaseBuffer.ITextEventRaiser raiser = projectionBuffer.PropagateSourceChanges(options, editTag);
+ this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, projectionBuffer));
+ }
+
+ this.graph = null;
+ this.buffer2EditMap = null;
+ this.masterBuffer = null;
+ this.pendingIndependentBuffers = null;
+ }
+ catch (Exception e)
+ {
+ BufferGroup.LastMasterEditException = e;
+ BufferGroup.LastMasterEditExceptionStackTrace = e.StackTrace;
+ throw;
+ }
+ }
+
+ public void ScheduleIndependentEdit(BaseProjectionBuffer projectionBuffer)
+ {
+ // A projection buffer has received a content change event from one of its source buffers, but
+ // it has no edit in progress. This means it is an independent buffer (with respect to the master edit)
+ // and needs to have its event propagation scheduled.
+ this.pendingIndependentBuffers.Add(projectionBuffer);
+ }
+
+ public void CancelIndependentEdit(BaseProjectionBuffer projectionBuffer)
+ {
+ // A buffer that thought it was independent turned out not to be so. May be called by buffers
+ // that are not in the pendingIndependentBuffers set.
+ this.pendingIndependentBuffers.Remove(projectionBuffer);
+ }
+
+ private void PopulateSourceEdits(ITextBuffer buffer)
+ {
+ IProjectionBufferBase projectionBuffer = buffer as IProjectionBufferBase;
+ if (projectionBuffer != null)
+ {
+ // Add ITextEdit objects to the edit map for all source buffers of the current projection buffer that
+ // are themselves projection buffers. This is so that such buffers are not considered independent buffers
+ // if it happens that the master edit does not touch them directly (even though not touched directly, they
+ // may receive events from their source buffers that were touched directly, and so FinalApply() will need
+ // to be invoked on them in order to construct a snapshot that matches the last snapshots of their source buffers).
+ foreach (ITextBuffer sourceBuffer in projectionBuffer.SourceBuffers)
+ {
+ IProjectionBufferBase sourceProjectionBuffer = sourceBuffer as IProjectionBufferBase;
+ if (sourceProjectionBuffer != null)
+ {
+ BaseBuffer baseSourceBuffer = (BaseBuffer)sourceBuffer;
+ if (!this.buffer2EditMap.ContainsKey(baseSourceBuffer))
+ {
+ this.buffer2EditMap.Add(sourceBuffer, (baseSourceBuffer.CreateSubordinateEdit(this.masterOptions, null, this.masterEditTag)));
+ }
+ }
+ }
+ }
+ }
+
+ private ISubordinateTextEdit PickEdit()
+ {
+ // find a buffer for which we are sure that all potential subordinate edits have been generated.
+
+ // this is an unsophisticated initial implementation. We are saved by the
+ // fact that real graphs are small (e.g. the typical venus graph has four buffers,
+ // five if outlining is involved)
+
+ foreach (var pair in this.buffer2EditMap)
+ {
+ BaseBuffer buffer = (BaseBuffer)pair.Key;
+ GraphEntry g;
+ if (!this.graph.TryGetValue(buffer, out g))
+ {
+ // if the buffer isn't in the graph, it is because it is being added to the graph
+ // as part of the current transaction (it must be the literal buffer of a projection buffer).
+ // We are cool to pick this buffer, which cannot possibly be affected by other edits.
+ this.buffer2EditMap.Remove(buffer);
+ return pair.Value;
+ }
+ else
+ {
+ if (InvulnerableToFutureEdits(g))
+ {
+ g.EditComplete = true;
+ this.buffer2EditMap.Remove(buffer); // can do better with indexed iteration
+ return pair.Value;
+ }
+ }
+ }
+
+ throw new InvalidOperationException("Internal error in BufferGroup.PickEdit");
+ }
+
+ private bool InvulnerableToFutureEdits(GraphEntry graphEntry)
+ {
+ foreach (IProjectionBufferBase target in graphEntry.Targets)
+ {
+ BaseBuffer baseTarget = (BaseBuffer)target;
+ GraphEntry targetEntry = this.graph[baseTarget];
+ if (targetEntry.EditComplete)
+ {
+ continue;
+ }
+ if (InvulnerableToFutureEdits(targetEntry) && !this.buffer2EditMap.ContainsKey(baseTarget))
+ {
+ targetEntry.EditComplete = true;
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private BaseProjectionBuffer PickIndependentBuffer()
+ {
+ // pick a buffer for which all source buffers have finished participating in the master edit.
+ // source buffer could be stable because
+ // 1. it is in the source closure of the master buffer -- all those edits are done by now
+ // 2. it isn't in the target closure of the master buffer
+ // 3. it has already been picked by this method
+
+ // for the moment, we are ignoring #2, and such buffers won't get picked (resulting in an exception).
+
+ foreach (BaseProjectionBuffer projectionBuffer in this.pendingIndependentBuffers)
+ {
+ bool ok = true;
+ foreach (ITextBuffer sourceBuffer in projectionBuffer.SourceBuffers)
+ {
+ if (!IsStableDuringIndependentPhase((BaseBuffer)sourceBuffer))
+ {
+ ok = false;
+ break;
+ }
+ }
+ if (ok)
+ {
+ GraphEntry gg = this.graph[(BaseBuffer)projectionBuffer];
+ gg.Dependent = true; // indicate has been chosen
+ this.pendingIndependentBuffers.Remove(projectionBuffer);
+ return projectionBuffer;
+ }
+ }
+ throw new InvalidOperationException("Couldn't pick an independent buffer");
+ }
+
+ private bool IsStableDuringIndependentPhase(BaseBuffer sourceBuffer)
+ {
+ BaseProjectionBuffer baseProjSourceBuffer = sourceBuffer as BaseProjectionBuffer;
+ if (baseProjSourceBuffer != null && this.pendingIndependentBuffers.Contains(baseProjSourceBuffer))
+ {
+ return false;
+ }
+ else
+ {
+ GraphEntry g;
+ if (this.graph.TryGetValue(sourceBuffer, out g))
+ {
+ return g.Dependent || !InTargetClosureOfBuffer(sourceBuffer, this.masterBuffer);
+ }
+ else
+ {
+ // must be a projection literal buffer
+ return true;
+ }
+ }
+ }
+
+ private bool InTargetClosureOfBuffer(BaseBuffer candidateBuffer, BaseBuffer governingBuffer)
+ {
+ GraphEntry g = this.graph[governingBuffer];
+ foreach (var target in g.Targets)
+ {
+ if (target == candidateBuffer)
+ {
+ return true;
+ }
+ if (InTargetClosureOfBuffer(candidateBuffer, (BaseBuffer)target))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ #region Graph Construction
+ private void BuildGraph()
+ {
+ // Police this.members by removing the expired ones
+ this.members.RemoveWhere((member) => member.Buffer == null);
+
+ this.graph = new Dictionary<BaseBuffer, GraphEntry>(this.members.Count);
+ foreach (BufferWeakReference bufferWeakReference in this.members)
+ {
+ var buffer = bufferWeakReference.Buffer;
+ if (buffer != null) //There could have been a GC between the police action above and here.
+ {
+ graph.Add(buffer, new GraphEntry(new HashSet<IProjectionBufferBase>(), false, false));
+ }
+ }
+ foreach (BufferWeakReference bufferWeakReference in this.members)
+ {
+ IProjectionBufferBase p = bufferWeakReference.Buffer as IProjectionBufferBase;
+ if (p != null)
+ {
+ foreach (BaseBuffer source in p.SourceBuffers)
+ {
+ graph[source].Targets.Add(p);
+ }
+ }
+ }
+ MarkMasterClosure(this.masterBuffer);
+ }
+
+ private void MarkMasterClosure(BaseBuffer buffer)
+ {
+ GraphEntry g = this.graph[buffer];
+ if (!g.Dependent)
+ {
+ g.Dependent = true;
+ IProjectionBufferBase projectionBuffer = buffer as IProjectionBufferBase;
+ if (projectionBuffer != null)
+ {
+ foreach (BaseBuffer source in projectionBuffer.SourceBuffers)
+ {
+ MarkMasterClosure(source);
+ }
+ }
+ }
+ }
+
+ public string DumpGraph()
+ {
+ StringBuilder sb = new StringBuilder("BufferGroup Graph");
+ if (this.graph == null)
+ {
+ sb.AppendLine(" <null>");
+ }
+ else
+ {
+ sb.AppendLine("");
+ foreach (var p in this.graph)
+ {
+ sb.Append(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0,8} {1}: ", TextUtilities.GetTag(p.Key), p.Value.EditComplete ? "T" : "F"));
+ foreach (var target in p.Value.Targets)
+ {
+ sb.Append(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0,8},", TextUtilities.GetTag(target)));
+ }
+ sb.Append("\r\n");
+ }
+ }
+ return sb.ToString();
+ }
+ #endregion
+
+ #region Edit and Event Management
+ public void BeginEdit()
+ {
+ if (depth < 0)
+ {
+ throw new System.InvalidOperationException();
+ }
+ depth++;
+ }
+
+ public void FinishEdit()
+ {
+ if (depth <= 0)
+ {
+ throw new System.InvalidOperationException();
+ }
+ if (--depth == 0)
+ {
+ RaiseEvents();
+ }
+ }
+
+ public void CancelEdit()
+ {
+ // if we were being transactional (that is, if we had multi-buffer transactions),
+ // we would cancel events here. But we aren't.
+ FinishEdit();
+ }
+
+ public void EnqueueEvents(BaseBuffer.ITextEventRaiser raiser, BaseBuffer baseBuffer)
+ {
+ if (depth <= 0)
+ {
+ throw new System.InvalidOperationException();
+ }
+ this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, baseBuffer));
+ }
+
+ public void EnqueueEvents(IEnumerable<BaseBuffer.ITextEventRaiser> raisers, BaseBuffer baseBuffer)
+ {
+ if (depth <= 0)
+ {
+ throw new System.InvalidOperationException();
+ }
+ foreach (var raiser in raisers)
+ {
+ this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, baseBuffer));
+ }
+ }
+
+ private void RaiseEvents()
+ {
+ if (!this.eventingInProgress)
+ {
+ List<BaseBuffer> buffersToRaisePostChangedEvent = new List<BaseBuffer>();
+ try
+ {
+ this.eventingInProgress = true;
+ while (this.eventQueue.Count > 0)
+ {
+ var pair = this.eventQueue.Dequeue();
+ pair.Item1.RaiseEvent(pair.Item2, false);
+
+ if (pair.Item1.HasPostEvent)
+ {
+ buffersToRaisePostChangedEvent.Add(pair.Item2);
+ }
+ }
+ }
+ finally
+ {
+ this.eventingInProgress = false;
+ }
+
+ foreach (var buffer in buffersToRaisePostChangedEvent)
+ {
+ // Raise the post changed event in the same order the standard events
+ // were raised
+ buffer.RaisePostChangedEvent();
+ }
+ }
+ }
+ #endregion
+
+ /// <summary>
+ /// Helper class that lets a WeakReference play in a HashSet in much the same way that a conventional pointer would.
+ /// The goal here is to let the BufferGroup keep a HashSet of its member buffers without preventing the members from
+ /// being GC'd if everyone else has forgotten about them.
+ /// </summary>
+ internal class BufferWeakReference
+ {
+ private readonly WeakReference<BaseBuffer> _buffer;
+ private readonly int _hashCode;
+
+ public BufferWeakReference(ITextBuffer buffer)
+ {
+ _buffer = new WeakReference<BaseBuffer>((BaseBuffer)buffer);
+ _hashCode = buffer.GetHashCode();
+ }
+
+ public BaseBuffer Buffer
+ {
+ get
+ {
+ BaseBuffer buffer;
+ if (_buffer.TryGetTarget(out buffer))
+ {
+ return buffer;
+ }
+
+ return null;
+ }
+ }
+
+ // Override the normal equality semantics so that two instances of a BufferWeakReference for the same buffer will be "equal"
+ public override bool Equals(object obj)
+ {
+ if (object.ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+
+ BufferWeakReference other = obj as BufferWeakReference;
+ if (other != null)
+ {
+ var myBuffer = this.Buffer;
+
+ // There are two interesting scenarios where we should return true:
+ // We have two different BufferWeakReferences that refer to the same buffer.
+ // We have a dead BufferWeakReference that we are removing from the list. In this case
+ // we'll return true on the object.ReferenceEquals above.
+ //
+ // Given how we are using the BufferWeakReference, we will never have the situation where
+ // there were two distinct instances of a BufferWeakReference for the same buffer, the buffer
+ // died, and an equality test was done on the two dead references.
+ //
+ // As it stands, either the source buffer is alive (because we're doing a
+ // this.members.Remove(new BufferWeakReference(member));
+ // (in which case the buffer in the WeakReference we are comparing it with is alive) or we're doing a
+ // this.members.RemoveWhere(b => ...)
+ // (in which case the ReferenceEquals test will work).
+ return (myBuffer == other.Buffer) && (myBuffer != null);
+ }
+
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return _hashCode;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/CachingTextImage.cs b/src/Text/Impl/TextModel/CachingTextImage.cs
new file mode 100644
index 0000000..5df0c21
--- /dev/null
+++ b/src/Text/Impl/TextModel/CachingTextImage.cs
@@ -0,0 +1,102 @@
+//
+// 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.Collections.Generic;
+
+ /// <summary>
+ /// Helper class that improves performances of ITextImage by caching last leaf accessed for certain operations.
+ /// </summary>
+ internal class CachingTextImage : ITextImage
+ {
+ public readonly StringRebuilder Builder;
+ private Tuple<int, StringRebuilder> _cache;
+
+ public static ITextImage Create(StringRebuilder builder, ITextImageVersion version)
+ {
+ return new CachingTextImage(builder, version);
+ }
+
+ private CachingTextImage(StringRebuilder builder, ITextImageVersion version)
+ {
+ if (builder == null)
+ throw new ArgumentNullException(nameof(builder));
+
+ this.Builder = builder;
+ this.Version = version;
+ }
+
+ public ITextImageVersion Version { get; }
+
+ public ITextImage GetSubText(Span span) { return CachingTextImage.Create(this.Builder.GetSubText(span), version: null); }
+
+ public int Length => this.Builder.Length;
+ public int LineCount => (this.Builder.LineBreakCount + 1);
+
+ public string GetText(Span span)
+ {
+ Tuple<int, StringRebuilder> cache = this.UpdateCache(span.Start);
+
+ int offsetStart = span.Start - cache.Item1;
+ if (offsetStart + span.Length < cache.Item2.Length)
+ {
+ return cache.Item2.GetText(new Span(offsetStart, span.Length));
+ }
+
+ return this.Builder.GetText(span);
+ }
+
+ public char[] ToCharArray(int startIndex, int length) { return this.Builder.ToCharArray(startIndex, length); }
+ public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { this.Builder.CopyTo(sourceIndex, destination, destinationIndex, count); }
+
+ public char this[int position]
+ {
+ get
+ {
+ Tuple<int, StringRebuilder> cache = this.UpdateCache(position);
+
+ return cache.Item2[position - cache.Item1];
+ }
+ }
+
+ public TextImageLine GetLineFromLineNumber(int lineNumber)
+ {
+ Span extent;
+ int lineBreakLength;
+
+ this.Builder.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength);
+ return new TextImageLine(this, lineNumber, extent, lineBreakLength);
+ }
+
+ public TextImageLine GetLineFromPosition(int position)
+ {
+ return this.GetLineFromLineNumber(this.Builder.GetLineNumberFromPosition(position));
+ }
+
+ public int GetLineNumberFromPosition(int position) { return this.Builder.GetLineNumberFromPosition(position); }
+ public void Write(System.IO.TextWriter writer, Span span) { this.Builder.Write(writer, span); }
+
+ private Tuple<int, StringRebuilder> UpdateCache(int position)
+ {
+ Tuple<int, StringRebuilder> cache = _cache;
+ if ((cache == null) || (position < cache.Item1) || (position >= (cache.Item1 + cache.Item2.Length)))
+ {
+ int offset;
+ StringRebuilder leaf = this.Builder.GetLeaf(position, out offset);
+
+ cache = new Tuple<int, StringRebuilder>(offset, leaf);
+
+ //Since cache is a class, cachedLeaf should update atomically.
+ _cache = cache;
+ }
+
+ return cache;
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd b/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd
new file mode 100644
index 0000000..bd17675
--- /dev/null
+++ b/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.BaseStringRebuilder">
+ <Position X="3.5" Y="0.5" Width="2.25" />
+ <Members>
+ <Method Name="Assemble" Hidden="true" />
+ <Method Name="CompareTo" Hidden="true" />
+ <Method Name="CopyTo" Hidden="true" />
+ <Method Name="Delete" Hidden="true" />
+ <Method Name="Equals" Hidden="true" />
+ <Method Name="GetEnumerator" Hidden="true" />
+ <Method Name="GetLineFromLineNumber" Hidden="true" />
+ <Method Name="GetLineNumberFromPosition" Hidden="true" />
+ <Method Name="GetText" Hidden="true" />
+ <Method Name="IEnumerable.GetEnumerator" Hidden="true" />
+ <Method Name="Insert" Hidden="true" />
+ <Property Name="Length" Hidden="true" />
+ <Property Name="LineBreakCount" Hidden="true" />
+ <Method Name="Replace" Hidden="true" />
+ <Method Name="Substring" Hidden="true" />
+ <Property Name="this" Hidden="true" />
+ <Method Name="ToCharArray" Hidden="true" />
+ <Method Name="Write" Hidden="true" />
+ </Members>
+ <NestedTypes>
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.BaseStringRebuilder.RebuilderEnumerator" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>StringRebuilder\BaseStringRebuilder.cs</NewMemberFileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ </NestedTypes>
+ <TypeIdentifier>
+ <HashCode>AAAAAMAAAEAAABQJiAAAAABAAAEAAAQECAAAAAEAKCA=</HashCode>
+ <FileName>StringRebuilder\BaseStringRebuilder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.BinaryStringRebuilder">
+ <Position X="1.5" Y="2.5" Width="2.25" />
+ <Members>
+ <Field Name="_crlf" Hidden="true" />
+ <Field Name="_depth" Hidden="true" />
+ <Field Name="_left" Hidden="true" />
+ <Field Name="_length" Hidden="true" />
+ <Field Name="_lineBreakCount" Hidden="true" />
+ <Field Name="_maxCharactersToConsolidate" Hidden="true" />
+ <Field Name="_maxLinesToConsolidate" Hidden="true" />
+ <Field Name="_right" Hidden="true" />
+ </Members>
+ <Compartments>
+ <Compartment Name="Nested Types" Collapsed="false" />
+ </Compartments>
+ <NestedTypes>
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.BinaryStringRebuilder.SimpleTreeNode" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>StringRebuilder\BinaryStringRebuilder.cs</NewMemberFileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ </NestedTypes>
+ <TypeIdentifier>
+ <HashCode>AAAMAAAAAEQAGMQNCAgAAABAACAAAUwgBAAAAAEAKAA=</HashCode>
+ <FileName>StringRebuilder\BinaryStringRebuilder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.SimpleStringRebuilder">
+ <Position X="5" Y="2.5" Width="2.75" />
+ <Members>
+ <Field Name="_empty" Hidden="true" />
+ <Field Name="_lineBreakSpans" Hidden="true" />
+ <Field Name="_source" Hidden="true" />
+ <Field Name="_span" Hidden="true" />
+ </Members>
+ <Compartments>
+ <Compartment Name="Nested Types" Collapsed="false" />
+ </Compartments>
+ <NestedTypes>
+ <Class Name="Microsoft.VisualStudio.Text.Implementation.SimpleStringRebuilder.LineBreak" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>StringRebuilder\SimpleStringRebuilder.cs</NewMemberFileName>
+ </TypeIdentifier>
+ </Class>
+ </NestedTypes>
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAEAACMQNCAABAABAAAAAAEQgACAAAAEAKAA=</HashCode>
+ <FileName>StringRebuilder\SimpleStringRebuilder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Interface Name="Microsoft.VisualStudio.Text.Implementation.IStringRebuilder">
+ <Position X="8.5" Y="0.5" Width="2.25" />
+ <TypeIdentifier>
+ <HashCode>AAAAAMAAAEAAAAQJCAAAAABAAAEAAAQAAAAAAAEAKCA=</HashCode>
+ <FileName>StringRebuilder\IStringRebuilder.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="Microsoft.VisualStudio.Text.Implementation.ITreeNode">
+ <Position X="11.5" Y="0.5" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAACMAAAAAAAAAAAAAAAAAgAAAAAAAAAAA=</HashCode>
+ <FileName>StringRebuilder\ITreeNode.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/EncodedStreamReader.cs b/src/Text/Impl/TextModel/EncodedStreamReader.cs
new file mode 100644
index 0000000..b11a34b
--- /dev/null
+++ b/src/Text/Impl/TextModel/EncodedStreamReader.cs
@@ -0,0 +1,134 @@
+//
+// 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.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal class EncodedStreamReader
+ {
+ /// <summary>
+ /// Open a stream reader from the specified stream, using the default encoding
+ /// unless either byte order marks are found (specifying a different encoding)
+ /// or -- if no byte order marks are found -- one of the given encoding detectors
+ /// can deduce an appropriate encoding.
+ ///
+ /// Note that the stream passed to OpenStreamReader must support both Peek and setting
+ /// the position.
+ /// </summary>
+ /// <param name="stream">stream on which to open the stream reader</param>
+ /// <returns>The detected encoding or null.</returns>
+ public static Encoding DetectEncoding(Stream stream,
+ List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> encodingDetectorExtensions,
+ GuardedOperations guardedOperations)
+ {
+ if (stream == null)
+ throw new ArgumentNullException("stream");
+
+ long position = stream.Position;
+
+ bool isStreamEmpty;
+
+ Encoding detectedEncoding = CheckForBoM(stream, out isStreamEmpty);
+
+ // If there was no BoM, try the detector extensions.
+ if (detectedEncoding == null && !isStreamEmpty)
+ {
+ detectedEncoding = SniffForEncoding(stream, encodingDetectorExtensions, guardedOperations);
+
+ //Rewind the stream.
+ stream.Position = position;
+ }
+
+ return detectedEncoding;
+ }
+
+ internal static Encoding CheckForBoM(Stream stream, out bool isStreamEmpty)
+ {
+ long position = stream.Position;
+
+ //Open a stream reader to check for byte order marks
+ //(we can't use an encoding that has byte order marks as the encoding for deciding whether or not).
+ using (StreamReader reader = new NonStreamClosingStreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true))
+ {
+ //We need to peek in order to force the stream reader to actually
+ //get the encoding.
+
+ //Ah, except that there is a bug in the handling of the utf-32be encoding and peek.
+ //(if you peek you get the byte order mark and a subsequent read will also get the byte order
+ //mark). Read does not have this problem. If peek starts working reliably, we can go back to
+ //using it and not have to recreate the StreamReader when we detect byte order marks.
+ int peekedChar = reader.Read();
+
+ isStreamEmpty = peekedChar == -1;
+
+ //Rewind the stream.
+ stream.Position = position;
+
+ if (reader.CurrentEncoding == Encoding.ASCII)
+ {
+ //No byte order marks were found.
+ return null;
+ }
+ else
+ {
+ System.Diagnostics.Debug.Assert(reader.CurrentEncoding.GetPreamble().Length > 0);
+ return reader.CurrentEncoding;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Class to act as a stream reader, but that doesn't close the stream on dispose.
+ /// </summary>
+ internal class NonStreamClosingStreamReader : StreamReader
+ {
+ internal NonStreamClosingStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks)
+ : base(stream, encoding, detectEncodingFromByteOrderMarks)
+ { }
+
+ protected override void Dispose(bool disposing)
+ {
+ // Force the base to not dispose the stream this reader was created with since it doesn't own it.
+ base.Dispose(false);
+ }
+ }
+
+ private static Encoding SniffForEncoding(Stream stream, List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> orderedEncodingDetectors, GuardedOperations guardedOperations)
+ {
+ long position = stream.Position;
+
+ foreach (Lazy<IEncodingDetector, IEncodingDetectorMetadata> sniffer in orderedEncodingDetectors)
+ {
+ Encoding encoding = null;
+ try
+ {
+ encoding = sniffer.Value.GetStreamEncoding(stream);
+ }
+ catch (Exception e)
+ {
+ guardedOperations.HandleException(sniffer, e);
+ }
+
+ //Rewind the stream
+ stream.Position = position;
+
+ //Return if we smelled something.
+ if (encoding != null)
+ return encoding;
+ }
+
+ return null;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs b/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs
new file mode 100644
index 0000000..dccc8bf
--- /dev/null
+++ b/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs
@@ -0,0 +1,51 @@
+//
+// 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.Text;
+
+ /// <summary>
+ /// Decoder that detects non-ASCII characters.
+ /// </summary>
+ class ExtendedCharacterDetectionDecoder : Decoder
+ {
+ private Decoder decoder;
+ private Action response;
+
+ internal ExtendedCharacterDetectionDecoder(Decoder decoder, Action response)
+ {
+ this.decoder = decoder;
+ this.response = response;
+ }
+
+ public override int GetCharCount(byte[] bytes, int index, int count)
+ {
+ return decoder.GetCharCount(bytes, index, count);
+ }
+
+ public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
+ {
+ int charCount = decoder.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
+ if (response != null)
+ {
+ int maxCharIndex = charIndex + charCount;
+ for (int i = charIndex; i < maxCharIndex; i++)
+ {
+ if (chars[i] > 0x7F)
+ {
+ response();
+ response = null;
+ break;
+ }
+ }
+ }
+ return charCount;
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs b/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs
new file mode 100644
index 0000000..dce88db
--- /dev/null
+++ b/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs
@@ -0,0 +1,37 @@
+//
+// 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.Text;
+
+ /// <summary>
+ /// Corresponds to UTF8-no-BOM encoding.
+ /// Determines whether multi-byte characters were decoded.
+ /// Throws on invalid bytes.
+ /// </summary>
+ internal class ExtendedCharacterDetector : UTF8Encoding
+ {
+ internal bool DecodedExtendedCharacters { get; private set; }
+
+ internal ExtendedCharacterDetector()
+ : base(false, true)
+ {
+ DecodedExtendedCharacters = false;
+ }
+
+ public override Decoder GetDecoder()
+ {
+ return new ExtendedCharacterDetectionDecoder(base.GetDecoder(), HandleExtendedCharacter);
+ }
+
+ private void HandleExtendedCharacter()
+ {
+ DecodedExtendedCharacters = true;
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/FallbackDetector.cs b/src/Text/Impl/TextModel/FallbackDetector.cs
new file mode 100644
index 0000000..5753980
--- /dev/null
+++ b/src/Text/Impl/TextModel/FallbackDetector.cs
@@ -0,0 +1,76 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ // The .NET Encoding DecoderFallbacks do not allow one to easily determine
+ // whether a fallback actually occurs so this type overrides whatever is necessary
+ // to detect it.
+ internal class FallbackDetector : DecoderFallback
+ {
+ private DecoderFallback decoderFallback;
+
+ internal bool FallbackOccurred { get; private set; }
+
+ public FallbackDetector(DecoderFallback decoderFallback)
+ {
+ this.decoderFallback = decoderFallback;
+ }
+
+ public override DecoderFallbackBuffer CreateFallbackBuffer()
+ {
+ var buffer = new FallbackBufferDetector(this.decoderFallback.CreateFallbackBuffer());
+ buffer.FallbackOccurred += (s, e) => this.FallbackOccurred = true;
+ return buffer;
+ }
+
+ public override int MaxCharCount
+ {
+ get { return this.decoderFallback.MaxCharCount; }
+ }
+
+ private class FallbackBufferDetector : DecoderFallbackBuffer
+ {
+ private DecoderFallbackBuffer inner;
+
+ internal event EventHandler FallbackOccurred;
+
+ internal FallbackBufferDetector(DecoderFallbackBuffer inner)
+ {
+ this.inner = inner;
+ }
+
+ public override bool Fallback(byte[] bytesUnknown, int index)
+ {
+ if (this.FallbackOccurred != null)
+ this.FallbackOccurred(this, EventArgs.Empty);
+
+ return this.inner.Fallback(bytesUnknown, index);
+ }
+
+ public override char GetNextChar()
+ {
+ return this.inner.GetNextChar();
+ }
+
+ public override bool MovePrevious()
+ {
+ return this.inner.MovePrevious();
+ }
+
+ public override int Remaining
+ {
+ get { return this.inner.Remaining; }
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/FileUtilities.cs b/src/Text/Impl/TextModel/FileUtilities.cs
new file mode 100644
index 0000000..2c567c3
--- /dev/null
+++ b/src/Text/Impl/TextModel/FileUtilities.cs
@@ -0,0 +1,257 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal class FileUtilities
+ {
+ public static void SaveSnapshot(ITextSnapshot snapshot,
+ FileMode fileMode,
+ Encoding encoding,
+ string filePath)
+ {
+ Debug.Assert((fileMode == FileMode.Create) || (fileMode == FileMode.CreateNew));
+
+ //Save the contents of the text buffer to disk.
+
+ string temporaryFilePath = null;
+ try
+ {
+ FileStream originalFileStream = null;
+ FileStream temporaryFileStream = FileUtilities.CreateFileStream(filePath, fileMode, out temporaryFilePath, out originalFileStream);
+ if (originalFileStream == null)
+ {
+ //The "normal" scenario: save the snapshot directly to disk. Either:
+ // there are no hard links to the target file so we can write the snapshot to the temporary and use File.Replace.
+ // we're creating a new file (in which case, temporaryFileStream is a misnomer: it is the stream for the file we are creating).
+ try
+ {
+ using (StreamWriter streamWriter = new StreamWriter(temporaryFileStream, encoding))
+ {
+ snapshot.Write(streamWriter);
+ }
+ }
+ finally
+ {
+ //This is somewhat redundant: disposing of streamWriter had the side-effect of disposing of temporaryFileStream
+ temporaryFileStream.Dispose();
+ temporaryFileStream = null;
+ }
+
+ if (temporaryFilePath != null)
+ {
+ //We were saving to the original file and already have a copy of the file on disk.
+ int remainingAttempts = 3;
+ do
+ {
+ try
+ {
+ //Replace the contents of filePath with the contents of the temporary using File.Replace to
+ //preserve the various attributes of the original file.
+ File.Replace(temporaryFilePath, filePath, null, true);
+ temporaryFilePath = null;
+
+ return;
+ }
+ catch (FileNotFoundException)
+ {
+ // The target file doesn't exist (someone deleted it after we detected it earlier).
+ // This is an acceptable condition so don't throw.
+ File.Move(temporaryFilePath, filePath);
+ temporaryFilePath = null;
+
+ return;
+ }
+ catch (IOException)
+ {
+ //There was some other exception when trying to replace the contents of the file
+ //(probably because some other process had the file locked).
+ //Wait a few ms and try again.
+ System.Threading.Thread.Sleep(5);
+ }
+ }
+ while (--remainingAttempts > 0);
+
+ //We're giving up on replacing the file. Try overwriting it directly (this is essentially the old Dev11 behavior).
+ //Do not try approach we are using for hard links (copying the original & restoring it if there is a failure) since
+ //getting here implies something strange is going on with the file system (Git or the like locking files) so we
+ //want the simplest possible fallback.
+
+ //Failing here causes the exception to be passed to the calling code.
+ using (FileStream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ using (StreamWriter streamWriter = new StreamWriter(stream, encoding))
+ {
+ snapshot.Write(streamWriter);
+ }
+ }
+ }
+ }
+ else
+ {
+ //filePath has hard links so we need to use a different approach to save the file:
+ // copy the original file to the temporary
+ // write directly to the original
+ // restore the original in the event of errors (which could be encoding errors and not disk issues) if there's a problem.
+ try
+ {
+ // Copy the contents of the original file to the temporary.
+ originalFileStream.CopyTo(temporaryFileStream);
+
+ //We've got a clean copy, try writing the snapshot directly to the original file
+ try
+ {
+ originalFileStream.Seek(0, SeekOrigin.Begin);
+ originalFileStream.SetLength(0);
+
+ //Make sure the StreamWriter is flagged leaveOpen == true. Otherwise disposing of the StreamWriter will dispose of originalFileStream and we need to
+ //leave originalFileStream open so we can use it to restore the original from the temporary copy we made.
+ using (var streamWriter = new StreamWriter(originalFileStream, encoding, bufferSize: 1024, leaveOpen: true)) //1024 == the default buffer size for a StreamWriter.
+ {
+ snapshot.Write(streamWriter);
+ }
+ }
+ catch
+ {
+ //Restore the original from the temporary copy we made (but rethrow the original exception since we didn't save the file).
+ temporaryFileStream.Seek(0, SeekOrigin.Begin);
+
+ originalFileStream.Seek(0, SeekOrigin.Begin);
+ originalFileStream.SetLength(0);
+
+ temporaryFileStream.CopyTo(originalFileStream);
+
+ throw;
+ }
+ }
+ finally
+ {
+ originalFileStream.Dispose();
+ originalFileStream = null;
+
+ temporaryFileStream.Dispose();
+ temporaryFileStream = null;
+ }
+ }
+ }
+ finally
+ {
+ if (temporaryFilePath != null)
+ {
+ try
+ {
+ //We do not need the temporary any longer.
+ if (File.Exists(temporaryFilePath))
+ {
+ File.Delete(temporaryFilePath);
+ }
+ }
+ catch
+ {
+ //Failing to clean up the temporary is an ignorable exception.
+ }
+ }
+ }
+ }
+
+ private static FileStream CreateFileStream(string filePath, FileMode fileMode, out string temporaryPath, out FileStream originalFileStream)
+ {
+ originalFileStream = null;
+
+ if (File.Exists(filePath))
+ {
+ // We're writing to a file that already exists. This is an error if we're trying to do a CreateNew.
+ if (fileMode == FileMode.CreateNew)
+ {
+ throw new IOException(filePath + " exists");
+ }
+
+ try
+ {
+ originalFileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
+
+ //Even thoug SafeFileHandle is an IDisposable, we don't dispose of it since that closes the strem.
+ var safeHandle = originalFileStream.SafeFileHandle;
+ if (!(safeHandle.IsClosed || safeHandle.IsInvalid))
+ {
+ BY_HANDLE_FILE_INFORMATION fi;
+ if (GetFileInformationByHandle(safeHandle, out fi))
+ {
+ if (fi.NumberOfLinks <= 1)
+ {
+ // The file we're trying to write to doesn't have any hard links ... clear out the originalFileStream
+ // as a clue.
+ originalFileStream.Dispose();
+ originalFileStream = null;
+ }
+ }
+ }
+ }
+ catch
+ {
+ if (originalFileStream != null)
+ {
+ originalFileStream.Dispose();
+ originalFileStream = null;
+ }
+
+ //We were not able to determine whether or not the file had hard links so throw here (aborting the save)
+ //since we don't know how to do it safely.
+ throw;
+ }
+
+ string root = Path.GetDirectoryName(filePath);
+
+ int count = 0;
+ while (++count < 20)
+ {
+ try
+ {
+ temporaryPath = Path.Combine(root, Path.GetRandomFileName() + "~"); //The ~ suffix hides the temporary file from GIT.
+ return new FileStream(temporaryPath, FileMode.CreateNew, (originalFileStream != null) ? FileAccess.ReadWrite : FileAccess.Write, FileShare.None);
+ }
+ catch (IOException)
+ {
+ //Ignore IOExceptions ... GetRandomFileName() came up with a duplicate so we need to try again.
+ }
+ }
+
+ Debug.Fail("Unable to create a temporary file");
+ }
+
+ temporaryPath = null;
+ return new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct BY_HANDLE_FILE_INFORMATION
+ {
+ public uint FileAttributes;
+ public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
+ public uint VolumeSerialNumber;
+ public uint FileSizeHigh;
+ public uint FileSizeLow;
+ public uint NumberOfLinks;
+ public uint FileIndexHigh;
+ public uint FileIndexLow;
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool GetFileInformationByHandle(
+ Microsoft.Win32.SafeHandles.SafeFileHandle hFile,
+ out BY_HANDLE_FILE_INFORMATION lpFileInformation
+ );
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs
new file mode 100644
index 0000000..4b8f0c2
--- /dev/null
+++ b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs
@@ -0,0 +1,38 @@
+//
+// 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;
+
+ internal sealed class ForwardFidelityCustomTrackingSpan : ForwardFidelityTrackingSpan
+ {
+ object customState;
+ CustomTrackToVersion behavior;
+
+ public ForwardFidelityCustomTrackingSpan(TextVersion version, Span span, object customState, CustomTrackToVersion behavior)
+ : base(version, span, SpanTrackingMode.Custom)
+ {
+ if (behavior == null)
+ {
+ throw new ArgumentNullException("behavior");
+ }
+ this.behavior = behavior;
+ this.customState = customState;
+ }
+
+ protected override Span TrackSpanForwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion)
+ {
+ return behavior(this, currentVersion, targetVersion, span, this.customState);
+ }
+
+ protected override Span TrackSpanBackwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion)
+ {
+ return behavior(this, currentVersion, targetVersion, span, this.customState);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs b/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs
new file mode 100644
index 0000000..5cbda46
--- /dev/null
+++ b/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs
@@ -0,0 +1,97 @@
+//
+// 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
+{
+ /// <summary>
+ /// Implementation of ITrackingPoint for the Forward TrackingFidelityMode.
+ /// Results of moving backwards in version space are not guaranteed to match results
+ /// for the same version when moving forward (i.e., no special support for noninvertible transitions).
+ /// No special support for Undo/Redo.
+ /// </summary>
+ internal class ForwardFidelityTrackingPoint : TrackingPoint
+ {
+ #region State and Construction
+ private class VersionPosition
+ {
+ private ITextVersion version;
+ private int position;
+
+ public VersionPosition(ITextVersion version, int position)
+ {
+ this.version = version;
+ this.position = position;
+ }
+
+ public ITextVersion Version { get { return this.version; } }
+ public int Position { get { return this.position; } }
+ }
+
+ private VersionPosition cachedPosition;
+
+ public ForwardFidelityTrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode)
+ : base(version, position, trackingMode)
+ {
+ this.cachedPosition = new VersionPosition(version, position);
+ }
+ #endregion
+
+ #region Overridden methods
+ public override ITextBuffer TextBuffer
+ {
+ get { return this.cachedPosition.Version.TextBuffer; }
+ }
+
+ public override TrackingFidelityMode TrackingFidelity
+ {
+ get { return TrackingFidelityMode.Forward; }
+ }
+
+ protected override int TrackPosition(ITextVersion targetVersion)
+ {
+ // Compute the new position on the requested snapshot.
+ //
+ // This method can be called simultaneously from multiple threads, and must be fast.
+ //
+ // We are relying on the atomicity of pointer copies (this.cachedPosition might change after we've
+ // fetched it but we will always get a self-consistent VersionPosition). This ensures we
+ // have proper behavior when called from multiple threads--multiple threads may all track and update the
+ // cached value if called at inconvenient times, but they will return consistent results.
+ // ForwardFidelity points do not support tracking backward, so consistency is not guaranteed in that case.
+
+ VersionPosition cached = this.cachedPosition;
+ int targetPosition;
+ if (targetVersion == cached.Version)
+ {
+ targetPosition = cached.Position;
+ }
+ else if (targetVersion.VersionNumber > cached.Version.VersionNumber)
+ {
+ // Roll the cached version forward to the requested version.
+ targetPosition = Tracking.TrackPositionForwardInTime(this.trackingMode, cached.Position, cached.Version, targetVersion);
+
+ // Cache new cached version.
+ this.cachedPosition = new VersionPosition(targetVersion, targetPosition);
+ }
+ else
+ {
+ // Roll backwards from the cached version.
+ targetPosition = Tracking.TrackPositionBackwardInTime(this.trackingMode, cached.Position, cached.Version, targetVersion);
+ }
+ return targetPosition;
+ }
+ #endregion
+
+ #region Diagnostic Support
+ public override string ToString()
+ {
+ VersionPosition c = this.cachedPosition;
+ return ToString(c.Version, c.Position, this.trackingMode);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs b/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs
new file mode 100644
index 0000000..2781cba
--- /dev/null
+++ b/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs
@@ -0,0 +1,112 @@
+//
+// 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
+{
+ /// <summary>
+ /// Implementation of ITrackingSpan for the Forward TrackingFidelityMode.
+ /// Results of moving backwards in version space are not guaranteed to match results
+ /// for the same version when moving forward (i.e., no special support for noninvertible transitions).
+ /// No special support for Undo/Redo.
+ /// </summary>
+ internal class ForwardFidelityTrackingSpan : TrackingSpan
+ {
+ #region State and Construction
+ private class VersionSpan
+ {
+ private ITextVersion version;
+ private Span span;
+
+ public VersionSpan(ITextVersion version, Span span)
+ {
+ this.version = version;
+ this.span = span;
+ }
+
+ public ITextVersion Version { get { return this.version; } }
+ public Span Span { get { return this.span; } }
+ }
+
+ private VersionSpan cachedSpan;
+
+ public ForwardFidelityTrackingSpan(ITextVersion version, Span span, SpanTrackingMode trackingMode)
+ : base(version, span, trackingMode)
+ {
+ this.cachedSpan = new VersionSpan(version, span);
+ }
+ #endregion
+
+ #region ITrackingSpan members
+ public override ITextBuffer TextBuffer
+ {
+ get { return this.cachedSpan.Version.TextBuffer; }
+ }
+
+ public override TrackingFidelityMode TrackingFidelity
+ {
+ get { return TrackingFidelityMode.Forward; }
+ }
+
+ protected override Span TrackSpan(ITextVersion targetVersion)
+ {
+ // Compute the new span on the requested snapshot.
+ //
+ // This method can be called simultaneously from multiple threads, and must be fast.
+ //
+ // We are relying on the atomicity of pointer copies (this.cachedSpan might change after we've
+ // fetched it but we will always get a self-consistent VersionPosition). This ensures we
+ // have proper behavior when called from multiple threads--multiple threads may all track and update the
+ // cached value if called at inconvenient times, but they will return consistent results.
+ // ForwardFidelity spans do not support tracking backward, so consistency is not guaranteed in that case.
+
+ VersionSpan cached = this.cachedSpan;
+ Span targetSpan;
+ if (targetVersion == cached.Version)
+ {
+ targetSpan = cached.Span;
+ }
+ else if (targetVersion.VersionNumber > cached.Version.VersionNumber)
+ {
+ // Compute the target span by going forward from the cached version
+ targetSpan = TrackSpanForwardInTime(cached.Span, cached.Version, targetVersion);
+
+ // Update the cached value
+ this.cachedSpan = new VersionSpan(targetVersion, targetSpan);
+ }
+ else
+ {
+ // Roll backwards from the cached version.
+ targetSpan = TrackSpanBackwardInTime(cached.Span, cached.Version, targetVersion);
+ }
+ return targetSpan;
+ }
+ #endregion
+
+ #region Helpers
+ protected virtual Span TrackSpanForwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion)
+ {
+ return Tracking.TrackSpanForwardInTime(this.trackingMode, span, currentVersion, targetVersion);
+ }
+
+ /// <summary>
+ /// Backward mapping. Used by all fidelity modes for mapping backwards under various circumstances.
+ /// </summary>
+ protected virtual Span TrackSpanBackwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion)
+ {
+ return Tracking.TrackSpanBackwardInTime(this.trackingMode, span, currentVersion, targetVersion);
+ }
+ #endregion
+
+ #region Diagnostic Support
+ public override string ToString()
+ {
+ VersionSpan c = this.cachedSpan;
+ return ToString(c.Version, c.Span, this.trackingMode);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs
new file mode 100644
index 0000000..db784fb
--- /dev/null
+++ b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs
@@ -0,0 +1,425 @@
+//
+// 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.Collections.Generic;
+ using System.Diagnostics;
+
+ /// <summary>
+ /// Implementation of ITrackingPoint for the UndoRedo and Backward TrackingFidelityMode.
+ /// Records noninvertible transition and consults these records as appropriate.
+ /// </summary>
+ internal class HighFidelityTrackingPoint : TrackingPoint
+ {
+ #region State and Construction
+ private class VersionPositionHistory
+ {
+ private readonly ITextVersion version;
+ private readonly int position;
+ private readonly List<VersionNumberPosition> noninvertibleHistory;
+
+ public VersionPositionHistory(ITextVersion version, int position, List<VersionNumberPosition> noninvertibleHistory)
+ {
+ this.version = version;
+ this.position = position;
+ this.noninvertibleHistory = noninvertibleHistory;
+ }
+
+ public ITextVersion Version { get { return this.version; } }
+ public int Position { get { return this.position; } }
+ public List<VersionNumberPosition> NoninvertibleHistory { get { return this.noninvertibleHistory; } }
+ }
+
+ private VersionPositionHistory cachedPosition;
+ private TrackingFidelityMode fidelity;
+
+ internal HighFidelityTrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode, TrackingFidelityMode fidelity)
+ : base(version, position, trackingMode)
+ {
+ if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward)
+ {
+ throw new ArgumentOutOfRangeException("fidelity");
+ }
+ List<VersionNumberPosition> initialHistory = null;
+ if (fidelity == TrackingFidelityMode.UndoRedo && version.VersionNumber > 0)
+ {
+ // The system may perform undo operations that reach prior to the initial version; if any of
+ // those transitions are noninvertible, then redoing back to the initial version will give the
+ // wrong answer. Thus we save the state of the point for the initial version, unless the
+ // initial version happens to be version zero (in which case we could not undo past it).
+
+ initialHistory = new List<VersionNumberPosition>();
+
+ if (version.VersionNumber != version.ReiteratedVersionNumber)
+ {
+ Debug.Assert(version.ReiteratedVersionNumber < version.VersionNumber);
+ // If the current version and reiterated version differ, also remember the position
+ // using the reiterated version number, since when undoing back to this point it
+ // will be the key that is used.
+ initialHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, position));
+ }
+
+ initialHistory.Add(new VersionNumberPosition(version.VersionNumber, position));
+ }
+ this.cachedPosition = new VersionPositionHistory(version, position, initialHistory);
+ this.fidelity = fidelity;
+ }
+ #endregion
+
+ #region Overrides
+ public override ITextBuffer TextBuffer
+ {
+ get { return this.cachedPosition.Version.TextBuffer; }
+ }
+
+ public override TrackingFidelityMode TrackingFidelity
+ {
+ get { return this.fidelity; }
+ }
+
+ protected override int TrackPosition(ITextVersion targetVersion)
+ {
+ // Compute the new position on the requested snapshot.
+ // This object caches the most recently requested version and the position in that version.
+ //
+ // We are relying on the atomicity of pointer copies (this.cachedPosition might change after we've
+ // fetched it below but we will always get a self-consistent VersionPosition). This ensures we
+ // have proper behavior when called from multiple threads (multiple threads may all track and update the
+ // cached value if called at inconvenient times, but they will return consistent results).
+ //
+ // In most cases, one can track backwards from the cached version to a previously computed
+ // version and get the same result, but this is not always the case: in particular, when the
+ // position lies in a deleted region, simulating reinsertion of that region will not cause
+ // the previous value of the position to be recovered. Such transitions are called noninvertible.
+ // This class explicitly tracks the positions of the point for versions for which the subsequent
+ // transition is noninvertible; this allows the value to be computed properly when tracking backwards
+ // or in undo/redo situations.
+
+ VersionPositionHistory cached = this.cachedPosition; // must fetch just once
+ if (targetVersion == cached.Version)
+ {
+ // easy!
+ return cached.Position;
+ }
+
+ List<VersionNumberPosition> noninvertibleHistory = cached.NoninvertibleHistory;
+ int targetPosition;
+ if (targetVersion.VersionNumber > cached.Version.VersionNumber)
+ {
+ // Roll the cached version forward to the requested version.
+ targetPosition = TrackPositionForwardInTime
+ (this.trackingMode,
+ this.fidelity,
+ ref noninvertibleHistory,
+ cached.Position,
+ cached.Version,
+ targetVersion);
+
+ // Cache new position
+ this.cachedPosition = new VersionPositionHistory(targetVersion, targetPosition, noninvertibleHistory);
+ }
+ else
+ {
+ // roll backwards from the cached version
+ targetPosition = TrackPositionBackwardInTime
+ (this.trackingMode,
+ this.fidelity == TrackingFidelityMode.Backward ? noninvertibleHistory : null,
+ cached.Position,
+ cached.Version,
+ targetVersion);
+ }
+ return targetPosition;
+ }
+ #endregion
+
+ internal static int TrackPositionForwardInTime(PointTrackingMode trackingMode,
+ TrackingFidelityMode fidelity,
+ ref List<VersionNumberPosition> noninvertibleHistory,
+ int currentPosition,
+ ITextVersion currentVersion,
+ ITextVersion targetVersion)
+ {
+ System.Diagnostics.Debug.Assert(targetVersion.VersionNumber > currentVersion.VersionNumber);
+
+ // track forward in time
+ ITextVersion roverVersion = currentVersion;
+ while (roverVersion != targetVersion)
+ {
+ currentVersion = roverVersion;
+ roverVersion = currentVersion.Next;
+
+ if (fidelity == TrackingFidelityMode.UndoRedo &&
+ roverVersion.ReiteratedVersionNumber != roverVersion.VersionNumber &&
+ noninvertibleHistory != null)
+ {
+ int p = noninvertibleHistory.BinarySearch
+ (new VersionNumberPosition(roverVersion.ReiteratedVersionNumber, 0),
+ VersionNumberPositionComparer.Instance);
+ if (p >= 0)
+ {
+ currentPosition = noninvertibleHistory[p].Position;
+ continue;
+ }
+ }
+
+ int changeCount = currentVersion.Changes.Count;
+ if (changeCount == 0)
+ {
+ // A version which has no text changes preserves the reiterated version number of the previous version,
+ // so its version number and reiterated version number will be different. We need to record a possibly
+ // noninvertible change with the reiterated version number as well as with the version number.
+ Debug.Assert(roverVersion.VersionNumber != roverVersion.ReiteratedVersionNumber);
+ if (roverVersion.VersionNumber != roverVersion.ReiteratedVersionNumber)
+ {
+ RecordNoninvertibleTransition(ref noninvertibleHistory, currentPosition, roverVersion.ReiteratedVersionNumber);
+ }
+ continue;
+ }
+
+ int currentVersionStartingPosition = currentPosition;
+ for (int c = 0; c < changeCount; ++c)
+ {
+ ITextChange textChange = currentVersion.Changes[c];
+ if (IsOpaque(textChange))
+ {
+ if (textChange.NewPosition + textChange.OldLength <= currentPosition)
+ {
+ // point is to the right of the opaque change. shift it.
+ currentPosition += textChange.Delta;
+ continue;
+ }
+ else if (textChange.NewPosition <= currentPosition)
+ {
+ // point is within the opaque change. stay put (but within the change)
+ if (textChange.NewEnd <= currentPosition)
+ {
+ // we shift to the left because the new text is shorter than what
+ // it replaced. this is a noninvertible transition, so remember it.
+ RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber);
+ currentPosition = textChange.NewEnd;
+ }
+ // No further change in this version can affect us.
+ break;
+ }
+ else
+ {
+ // point is to the left of the opaque change. no effect, and we are done.
+ break;
+ }
+ }
+
+ if (trackingMode == PointTrackingMode.Positive)
+ {
+ // Positive tracking mode: point moves with insertions at its location.
+ if (textChange.NewPosition <= currentPosition)
+ {
+ if (textChange.NewPosition + textChange.OldLength <= currentPosition)
+ {
+ // easy: the change is entirely to the left of our position.
+ // if NewPosition + OldLength == currentPosition, it means that the character
+ // immediately before our position was deleted (or replaced). When going back
+ // in time we will see this as an insertion, and since our tracking mode is positive,
+ // we will put ourselves after it.
+ currentPosition += textChange.Delta;
+ }
+ else
+ {
+ // textChange deleted text at the position of the tracked point; now the point is
+ // pushed to the end of the textChange insertion.
+
+ // This is a noninvertible transition. Remember it.
+ RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber);
+ currentPosition = textChange.NewEnd;
+ break;
+ }
+ }
+ else
+ {
+ // the change is entirely to the right of our position; since changes are
+ // sorted, we are done.
+ break;
+ }
+ }
+ else
+ {
+ // Negative tracking mode: point doesn't move with respect to insertions at its location.
+ if (textChange.NewPosition < currentPosition)
+ {
+ if (textChange.NewPosition + textChange.OldLength < currentPosition)
+ {
+ // easy: the change is entirely to the left of our position. Since we
+ // are paying attention to the tracking mode when tracking back (to handle the
+ // before-the-origin-version case), we need to consider NewPosition + OldLength ==
+ // currentPosition case as noninvertible.
+ currentPosition += textChange.Delta;
+ }
+ else
+ {
+ // textChange deleted text at the position of the tracked point; now the point is
+ // at the position of the change, prior to any insertion.
+
+ // This is a nonivertible transition.
+ RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber);
+ currentPosition = textChange.NewPosition;
+ break;
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ return currentPosition;
+ }
+
+ private static void RecordNoninvertibleTransition(ref List<VersionNumberPosition> noninvertibleHistory, int currentPosition, int versionNumber)
+ {
+ if (noninvertibleHistory == null)
+ {
+ noninvertibleHistory = new List<VersionNumberPosition>();
+ }
+ if (noninvertibleHistory.Count > 0 && noninvertibleHistory[noninvertibleHistory.Count - 1].VersionNumber == versionNumber)
+ {
+ // We already recorded the version/position, either because it's the first version
+ // or because of a reiterated version anomaly due to a version with no text changes
+ Debug.Assert(currentPosition == noninvertibleHistory[noninvertibleHistory.Count - 1].Position);
+ return;
+ }
+ noninvertibleHistory.Add(new VersionNumberPosition(versionNumber, currentPosition));
+
+ // this list had better be sorted!
+ Debug.Assert((noninvertibleHistory.Count == 1) ||
+ (noninvertibleHistory[noninvertibleHistory.Count - 1].VersionNumber >
+ noninvertibleHistory[noninvertibleHistory.Count - 2].VersionNumber));
+ }
+
+ internal static int TrackPositionBackwardInTime(PointTrackingMode trackingMode,
+ List<VersionNumberPosition> noninvertibleHistory,
+ int currentPosition,
+ ITextVersion currentVersion,
+ ITextVersion targetVersion)
+ {
+ System.Diagnostics.Debug.Assert(targetVersion.VersionNumber < currentVersion.VersionNumber);
+
+ ITextVersion[] textVersionsStack = new ITextVersion[currentVersion.VersionNumber - targetVersion.VersionNumber];
+ int top = 0;
+ {
+ ITextVersion roverVersion = targetVersion;
+ while (roverVersion != currentVersion)
+ {
+ textVersionsStack[top++] = roverVersion;
+ roverVersion = roverVersion.Next;
+ }
+ }
+
+ while (top > 0)
+ {
+ ITextVersion textVersion = textVersionsStack[--top];
+
+ if (noninvertibleHistory != null)
+ {
+ int p = noninvertibleHistory.BinarySearch
+ (new VersionNumberPosition(textVersion.VersionNumber, 0), VersionNumberPositionComparer.Instance);
+ if (p >= 0)
+ {
+ // the undo or redo change was noninvertible, so we've recorded the state before the change
+ currentPosition = noninvertibleHistory[p].Position;
+ continue;
+ }
+ }
+
+ IList<ITextChange> textChanges = textVersion.Changes;
+ for (int tc = textChanges.Count - 1; tc >= 0; --tc)
+ {
+ ITextChange textChange = textChanges[tc];
+ if (IsOpaque(textChange))
+ {
+ if (textChange.NewEnd <= currentPosition)
+ {
+ // point is to the right of the opaque change. shift it.
+ currentPosition -= textChange.Delta;
+ }
+ else if (textChange.NewPosition <= currentPosition)
+ {
+ // point is within the opaque change. stay put (but within the change)
+ currentPosition = Math.Min(currentPosition, textChange.NewPosition + textChange.OldLength);
+ }
+ continue;
+ }
+
+ // tracking modes don't matter when going back over ground we've previously traversed,
+ // because we are really reversing a deletion whose behavior had nothing to do with the
+ // tracking mode, and the correct answer is captured in the noninvertible history. However,
+ // when going back in time before the tracking point was created, it makes sense to honor
+ // the tracking mode in the absence of any other information.
+ if (trackingMode == PointTrackingMode.Positive)
+ {
+ // Positive tracking mode: point moves with insertions at its location.
+ if (textChange.NewPosition <= currentPosition)
+ {
+ if (textChange.NewEnd <= currentPosition)
+ {
+ currentPosition -= textChange.Delta;
+ }
+ else
+ {
+ currentPosition = textChange.NewPosition + textChange.OldLength;
+ }
+ }
+ }
+ else
+ {
+ if (textChange.NewPosition < currentPosition)
+ {
+ if (textChange.NewEnd < currentPosition)
+ {
+ currentPosition -= textChange.Delta;
+ }
+ else
+ {
+ // this is why we run the changes in reverse...
+ currentPosition = textChange.NewPosition;
+ }
+ }
+ }
+ }
+ }
+
+ return currentPosition;
+ }
+
+ private static bool IsOpaque(ITextChange textChange)
+ {
+ ITextChange2 tc2 = textChange as ITextChange2;
+ return tc2 != null && tc2.IsOpaque;
+ }
+
+ #region Diagnostic Support
+ public override string ToString()
+ {
+ VersionPositionHistory c = this.cachedPosition;
+ System.Text.StringBuilder sb = new System.Text.StringBuilder("*");
+ sb.Append(ToString(c.Version, c.Position, this.trackingMode));
+ if (c.NoninvertibleHistory != null)
+ {
+ sb.Append("[");
+ foreach (VersionNumberPosition vp in c.NoninvertibleHistory)
+ {
+ sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position));
+ }
+ sb.Append("]");
+ }
+ return sb.ToString();
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs
new file mode 100644
index 0000000..6eb6ca4
--- /dev/null
+++ b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs
@@ -0,0 +1,190 @@
+//
+// 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.Collections.Generic;
+ using System.Diagnostics;
+
+ /// <summary>
+ /// Implementation of ITrackingSpan for the UndoRedo and Backward TrackingFidelityMode.
+ /// Records noninvertible transition and consults these records as appropriate.
+ /// </summary>
+ internal class HighFidelityTrackingSpan : TrackingSpan
+ {
+ #region State and Construction
+ private class VersionSpanHistory
+ {
+ private readonly ITextVersion version;
+ private readonly Span span;
+ private readonly List<VersionNumberPosition> noninvertibleStartHistory;
+ private readonly List<VersionNumberPosition> noninvertibleEndHistory;
+
+ public VersionSpanHistory(ITextVersion version,
+ Span span,
+ List<VersionNumberPosition> noninvertibleStartHistory,
+ List<VersionNumberPosition> noninvertibleEndHistory)
+ {
+ this.version = version;
+ this.span = span;
+ this.noninvertibleStartHistory = noninvertibleStartHistory;
+ this.noninvertibleEndHistory = noninvertibleEndHistory;
+ }
+
+ public ITextVersion Version { get { return this.version; } }
+ public Span Span { get { return this.span; } }
+ public List<VersionNumberPosition> NoninvertibleStartHistory { get { return this.noninvertibleStartHistory; } }
+ public List<VersionNumberPosition> NoninvertibleEndHistory { get { return this.noninvertibleEndHistory; } }
+ }
+
+ private VersionSpanHistory cachedSpan;
+ private TrackingFidelityMode fidelity;
+
+ internal HighFidelityTrackingSpan(ITextVersion version, Span span, SpanTrackingMode spanTrackingMode, TrackingFidelityMode fidelity)
+ : base(version, span, spanTrackingMode)
+ {
+ if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward)
+ {
+ throw new ArgumentOutOfRangeException("fidelity");
+ }
+ List<VersionNumberPosition> startHistory = null;
+ List<VersionNumberPosition> endHistory = null;
+ if (fidelity == TrackingFidelityMode.UndoRedo && version.VersionNumber > 0)
+ {
+ // The system may perform undo operations that reach prior to the initial version; if any of
+ // those transitions are noninvertible, then redoing back to the initial version will give the
+ // wrong answer. Thus we save the state of the span for the initial version, unless
+ // the initial version happens to be version zero (in which case we could not undo past it).
+
+ startHistory = new List<VersionNumberPosition>();
+ endHistory = new List<VersionNumberPosition>();
+
+ if (version.VersionNumber != version.ReiteratedVersionNumber)
+ {
+ Debug.Assert(version.ReiteratedVersionNumber < version.VersionNumber);
+ // If the current version and reiterated version differ, also remember the position
+ // using the reiterated version number, since when undoing back to this point it
+ // will be the key that is used.
+ startHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, span.Start));
+ endHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, span.End));
+ }
+
+ startHistory.Add(new VersionNumberPosition(version.VersionNumber, span.Start));
+ endHistory.Add(new VersionNumberPosition(version.VersionNumber, span.End));
+ }
+ this.cachedSpan = new VersionSpanHistory(version, span, startHistory, endHistory);
+ this.fidelity = fidelity;
+ }
+ #endregion
+
+ #region Overrides
+ public override ITextBuffer TextBuffer
+ {
+ get { return this.cachedSpan.Version.TextBuffer; }
+ }
+
+ public override TrackingFidelityMode TrackingFidelity
+ {
+ get { return this.fidelity; }
+ }
+
+ protected override Span TrackSpan(ITextVersion targetVersion)
+ {
+ // Compute the span on the requested snapshot.
+ // This object caches the most recently requested version and the span in that version.
+ //
+ // We are relying on the atomicity of pointer copies (this.cachedSpan might change after we've
+ // fetched it below but we will always get a self-consistent VersionSpan). This ensures we
+ // have proper behavior when called from multiple threads (multiple threads may all track and update the
+ // cached value if called at inconvenient times, but they will return consistent results).
+ //
+ // In most cases, one can track backwards from the cached version to a previously computed
+ // version and get the same result, but this is not always the case: in particular, when one or both
+ // ends of the span lie in a deleted region, simulating reinsertion of that region will not cause
+ // the previous value of the span to be recovered. Such transitions are called noninvertible.
+ // This class explicitly tracks the positions of span endpoints for versions for which the subsequent
+ // transition is noninvertible; this allows the value to be computed properly when tracking backwards
+ // or in undo/redo situations.
+
+ VersionSpanHistory cached = this.cachedSpan; // must fetch just once
+ if (targetVersion == cached.Version)
+ {
+ // easy!
+ return cached.Span;
+ }
+
+ PointTrackingMode startMode =
+ (this.trackingMode == SpanTrackingMode.EdgeExclusive || this.trackingMode == SpanTrackingMode.EdgePositive)
+ ? PointTrackingMode.Positive
+ : PointTrackingMode.Negative;
+
+ PointTrackingMode endMode =
+ (this.trackingMode == SpanTrackingMode.EdgeExclusive || this.trackingMode == SpanTrackingMode.EdgeNegative)
+ ? PointTrackingMode.Negative
+ : PointTrackingMode.Positive;
+
+ List<VersionNumberPosition> noninvertibleStartHistory = cached.NoninvertibleStartHistory;
+ List<VersionNumberPosition> noninvertibleEndHistory = cached.NoninvertibleEndHistory;
+
+ Span targetSpan;
+ if (targetVersion.VersionNumber > cached.Version.VersionNumber)
+ {
+ // Compute the target span by going forward from the cached version
+ int start = HighFidelityTrackingPoint.TrackPositionForwardInTime
+ (startMode, this.fidelity, ref noninvertibleStartHistory, cached.Span.Start, cached.Version, targetVersion);
+ int end = HighFidelityTrackingPoint.TrackPositionForwardInTime
+ (endMode, this.fidelity, ref noninvertibleEndHistory, cached.Span.End, cached.Version, targetVersion);
+ targetSpan = Span.FromBounds(start, System.Math.Max(start, end));
+
+ // Cache the new span
+ this.cachedSpan = new VersionSpanHistory(targetVersion, targetSpan, noninvertibleStartHistory, noninvertibleEndHistory);
+ }
+ else
+ {
+ // we are looking for a version prior to the cached version.
+ int start = HighFidelityTrackingPoint.TrackPositionBackwardInTime
+ (startMode, this.fidelity == TrackingFidelityMode.Backward ? noninvertibleStartHistory : null, cached.Span.Start, cached.Version, targetVersion);
+ int end = HighFidelityTrackingPoint.TrackPositionBackwardInTime
+ (endMode, this.fidelity == TrackingFidelityMode.Backward ? noninvertibleEndHistory : null, cached.Span.End, cached.Version, targetVersion);
+
+ targetSpan = Span.FromBounds(start, System.Math.Max(start, end));
+ }
+ return targetSpan;
+ }
+ #endregion
+
+ #region Diagnostic Support
+
+ public override string ToString()
+ {
+ VersionSpanHistory c = this.cachedSpan;
+ System.Text.StringBuilder sb = new System.Text.StringBuilder("*");
+ sb.Append(ToString(c.Version, c.Span, this.trackingMode));
+ if (c.NoninvertibleStartHistory != null)
+ {
+ sb.Append("[Start");
+ foreach (VersionNumberPosition vp in c.NoninvertibleStartHistory)
+ {
+ sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position));
+ }
+ sb.Append("]");
+ }
+ if (c.NoninvertibleEndHistory != null)
+ {
+ sb.Append("[End");
+ foreach (VersionNumberPosition vp in c.NoninvertibleEndHistory)
+ {
+ sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position));
+ }
+ sb.Append("]");
+ }
+ return sb.ToString();
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs b/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs
new file mode 100644
index 0000000..4707f90
--- /dev/null
+++ b/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs
@@ -0,0 +1,25 @@
+//
+// 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 Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Subset of ITextBufferFactoryService. Used to avoid having to mock the asset system in unit tests.
+ /// </summary>
+ internal interface IInternalTextBufferFactory
+ {
+ ITextBuffer CreateTextBuffer(string text, IContentType contentType);
+
+ ITextBuffer CreateTextBuffer(string text, IContentType contentType, bool spurnGroup);
+
+ IContentType TextContentType { get; }
+ IContentType InertContentType { get; }
+ IContentType ProjectionContentType { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/ISubordinateTextEdit.cs b/src/Text/Impl/TextModel/ISubordinateTextEdit.cs
new file mode 100644
index 0000000..91d7236
--- /dev/null
+++ b/src/Text/Impl/TextModel/ISubordinateTextEdit.cs
@@ -0,0 +1,56 @@
+//
+// 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;
+
+ /// <summary>
+ /// These methods augment ITextEdit to support editing of source buffers of projection and elision buffers.
+ /// </summary>
+ internal interface ISubordinateTextEdit
+ {
+ /// <summary>
+ /// Compute effects of an edit on source buffers, if any, and add the source edits to the BufferGroup.
+ /// </summary>
+ void PreApply();
+
+ /// <summary>
+ /// Checks whether the edit on the buffer is allowed to continue.
+ /// </summary>
+ /// <param name="cancelAction">Action to perform immediately upon edit cancelation.</param>
+ /// <returns>True if the edit can continue.</returns>
+ bool CheckForCancellation(Action cancelAction);
+
+ /// <summary>
+ /// Commit effects of the edit, applying them to source buffers (if any).
+ /// </summary>
+ void FinalApply();
+
+ /// <summary>
+ /// The <see cref="ITextBuffer"/> to which this edit applies.
+ /// </summary>
+ ITextBuffer TextBuffer { get; }
+
+ /// <summary>
+ /// Restores any edit-in-progress state on the associated buffer.
+ /// </summary>
+ void CancelApplication();
+
+ /// <summary>
+ /// Whether the edit has been canceled.
+ /// </summary>
+ bool Canceled { get; }
+
+ /// <summary>
+ /// Mark the latest change in the edit as corresponding to a particular part of a master edit.
+ /// </summary>
+ /// <param name="masterChangeOffset">The index into the master edit's inserted text
+ /// that corresponds to the text that's being inserted by this subordinate edit.</param>
+ void RecordMasterChangeOffset(int masterChangeOffset);
+ }
+}
diff --git a/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs b/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs
new file mode 100644
index 0000000..26eda40
--- /dev/null
+++ b/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs
@@ -0,0 +1,33 @@
+//
+// 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;
+
+ /// <summary>
+ /// Describe the context of some text change with respect to compound line breaks.
+ /// </summary>
+ [Flags]
+ internal enum LineBreakBoundaryConditions : byte
+ {
+ /// <summary>
+ /// The change is neither preceded by a return character nor followed by a newline character.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// The change is immediately preceded by a return character.
+ /// </summary>
+ PrecedingReturn = 0x1,
+
+ /// <summary>
+ /// The change is followed immediately by a newline character.
+ /// </summary>
+ SucceedingNewline = 0x2
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/MappingPoint.cs b/src/Text/Impl/TextModel/MappingPoint.cs
new file mode 100644
index 0000000..46e34ec
--- /dev/null
+++ b/src/Text/Impl/TextModel/MappingPoint.cs
@@ -0,0 +1,153 @@
+//
+// 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 Microsoft.VisualStudio.Text.Projection;
+
+ internal partial class MappingPoint : IMappingPoint
+ {
+ SnapshotPoint anchorPoint;
+ PointTrackingMode trackingMode;
+ IBufferGraph bufferGraph;
+
+ public MappingPoint(SnapshotPoint anchorPoint, PointTrackingMode trackingMode, IBufferGraph bufferGraph)
+ {
+ if (anchorPoint.Snapshot == null)
+ {
+ throw new ArgumentNullException("anchorPoint");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (bufferGraph == null)
+ {
+ throw new ArgumentNullException("bufferGraph");
+ }
+ this.anchorPoint = anchorPoint;
+ this.trackingMode = trackingMode;
+ this.bufferGraph = bufferGraph;
+ }
+
+ public ITextBuffer AnchorBuffer
+ {
+ get { return this.anchorPoint.Snapshot.TextBuffer; }
+ }
+
+ public IBufferGraph BufferGraph
+ {
+ get { return this.bufferGraph; }
+ }
+
+ public SnapshotPoint? GetPoint(ITextBuffer targetBuffer, PositionAffinity affinity)
+ {
+ if (targetBuffer == null)
+ {
+ throw new ArgumentNullException("targetBuffer");
+ }
+ ITextBuffer anchorBuffer = this.AnchorBuffer;
+ SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode);
+ if (anchorBuffer == targetBuffer)
+ {
+ return currentPoint;
+ }
+
+ ITextBuffer topBuffer = this.bufferGraph.TopBuffer;
+ if (targetBuffer == topBuffer)
+ {
+ return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, topBuffer);
+ }
+ else if (anchorBuffer == topBuffer)
+ {
+ return this.bufferGraph.MapDownToBuffer(currentPoint, this.trackingMode, targetBuffer, affinity);
+ }
+ else
+ {
+ // we don't know a priori which way to go, so we'll guess
+ if (anchorBuffer is IProjectionBufferBase)
+ {
+ SnapshotPoint? tentative = this.bufferGraph.MapDownToBuffer(currentPoint, this.trackingMode, targetBuffer, affinity);
+ if (tentative.HasValue)
+ {
+ return tentative;
+ }
+ }
+ // ok, go the other way
+ return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, targetBuffer);
+ }
+ }
+
+ public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity)
+ {
+ if (targetSnapshot == null)
+ throw new ArgumentNullException("targetSnapshot");
+
+ SnapshotPoint? result = GetPoint(targetSnapshot.TextBuffer, affinity);
+ if (result.HasValue && (result.Value.Snapshot != targetSnapshot))
+ {
+ result = result.Value.TranslateTo(targetSnapshot, this.trackingMode);
+ }
+
+ return result;
+ }
+
+ public SnapshotPoint? GetPoint(Predicate<ITextBuffer> match, PositionAffinity affinity)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ ITextBuffer anchorBuffer = this.AnchorBuffer;
+ SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode);
+ if (match(anchorBuffer))
+ {
+ return currentPoint;
+ }
+
+ if (anchorBuffer == this.bufferGraph.TopBuffer)
+ {
+ // the only way to go is down
+ return this.bufferGraph.MapDownToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity);
+ }
+ else
+ {
+ // guess which way to go
+ if (anchorBuffer is IProjectionBufferBase)
+ {
+ SnapshotPoint? tentative = this.bufferGraph.MapDownToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity);
+ if (tentative.HasValue)
+ {
+ return tentative;
+ }
+ }
+ // go the other way.
+ if (match(this.bufferGraph.TopBuffer))
+ {
+ return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, this.bufferGraph.TopBuffer);
+ }
+ else
+ {
+ return this.bufferGraph.MapUpToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity);
+ }
+ }
+ }
+
+ public SnapshotPoint? GetInsertionPoint(Predicate<ITextBuffer> match)
+ {
+ // always maps down
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ ITextBuffer anchorBuffer = this.AnchorBuffer;
+ SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode);
+ return this.bufferGraph.MapDownToInsertionPoint(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/MappingSpan.cs b/src/Text/Impl/TextModel/MappingSpan.cs
new file mode 100644
index 0000000..a42f0fe
--- /dev/null
+++ b/src/Text/Impl/TextModel/MappingSpan.cs
@@ -0,0 +1,162 @@
+//
+// 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.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal partial class MappingSpan : IMappingSpan
+ {
+ private SnapshotSpan anchorSpan;
+ private SpanTrackingMode trackingMode;
+ private IBufferGraph bufferGraph;
+
+ public MappingSpan(SnapshotSpan anchorSpan, SpanTrackingMode trackingMode, IBufferGraph bufferGraph)
+ {
+ if (anchorSpan.Snapshot == null)
+ {
+ throw new ArgumentNullException("anchorSpan");
+ }
+ if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (bufferGraph == null)
+ {
+ throw new ArgumentNullException("bufferGraph");
+ }
+ this.anchorSpan = anchorSpan;
+ this.trackingMode = trackingMode;
+ this.bufferGraph = bufferGraph;
+ }
+
+ public IMappingPoint Start
+ {
+ get { return new MappingPoint(new SnapshotPoint(this.anchorSpan.Snapshot, this.anchorSpan.Start),
+ (this.trackingMode == SpanTrackingMode.EdgeInclusive ||
+ this.trackingMode == SpanTrackingMode.EdgeNegative)
+ ? PointTrackingMode.Negative
+ : PointTrackingMode.Positive,
+ this.bufferGraph); }
+ }
+
+ public IMappingPoint End
+ {
+ get { return new MappingPoint(new SnapshotPoint(this.anchorSpan.Snapshot, this.anchorSpan.End),
+ (this.trackingMode == SpanTrackingMode.EdgeExclusive ||
+ this.trackingMode == SpanTrackingMode.EdgeNegative)
+ ? PointTrackingMode.Negative
+ : PointTrackingMode.Positive,
+ this.bufferGraph);
+ }
+ }
+
+ public ITextBuffer AnchorBuffer
+ {
+ get { return this.anchorSpan.Snapshot.TextBuffer; }
+ }
+
+ public IBufferGraph BufferGraph
+ {
+ get { return this.bufferGraph; }
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(ITextBuffer targetBuffer)
+ {
+ // null textBuffer check will be handled by the buffer graph
+ ITextBuffer anchorBuffer = this.AnchorBuffer;
+ SnapshotSpan currentSpan = this.anchorSpan.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode);
+
+ if (anchorBuffer == targetBuffer)
+ {
+ return new NormalizedSnapshotSpanCollection(currentSpan);
+ }
+
+ ITextBuffer topBuffer = this.bufferGraph.TopBuffer;
+ if (targetBuffer == topBuffer)
+ {
+ return this.bufferGraph.MapUpToBuffer(currentSpan, this.trackingMode, topBuffer);
+ }
+ else if (anchorBuffer == topBuffer)
+ {
+ return this.bufferGraph.MapDownToBuffer(currentSpan, this.trackingMode, targetBuffer);
+ }
+ else
+ {
+ if (anchorBuffer is IProjectionBufferBase)
+ {
+ NormalizedSnapshotSpanCollection tentative = this.bufferGraph.MapDownToBuffer(currentSpan, this.trackingMode, targetBuffer);
+ if (tentative.Count > 0)
+ {
+ return tentative;
+ }
+ }
+ return this.bufferGraph.MapUpToBuffer(currentSpan, this.trackingMode, targetBuffer);
+ }
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot)
+ {
+ if (targetSnapshot == null)
+ throw new ArgumentNullException("targetSnapshot");
+
+ NormalizedSnapshotSpanCollection results = GetSpans(targetSnapshot.TextBuffer);
+ if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot))
+ {
+ FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan s in results)
+ {
+ translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode));
+ }
+
+ results = new NormalizedSnapshotSpanCollection(translatedSpans);
+ }
+
+ return results;
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(Predicate<ITextBuffer> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+
+ ITextBuffer anchorBuffer = this.AnchorBuffer;
+ SnapshotSpan currentSpan = this.anchorSpan.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode);
+ if (match(anchorBuffer))
+ {
+ return new NormalizedSnapshotSpanCollection(currentSpan);
+ }
+ if (anchorBuffer == this.bufferGraph.TopBuffer)
+ {
+ return this.bufferGraph.MapDownToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer)));
+ }
+ else
+ {
+ // guess which way to go
+ if (anchorBuffer is IProjectionBufferBase)
+ {
+ NormalizedSnapshotSpanCollection tentative = this.bufferGraph.MapDownToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer)));
+ if (tentative.Count > 0)
+ {
+ return tentative;
+ }
+ }
+ return this.bufferGraph.MapUpToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer)));
+ }
+ }
+
+ public override string ToString()
+ {
+ return String.Format("MappingSpan anchored at {0}", this.anchorSpan);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs
new file mode 100644
index 0000000..8e3352f
--- /dev/null
+++ b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs
@@ -0,0 +1,523 @@
+//
+// 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.Collections;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal partial class NormalizedTextChangeCollection : INormalizedTextChangeCollection
+ {
+ public static readonly NormalizedTextChangeCollection Empty = new NormalizedTextChangeCollection(new TextChange[0]);
+ private readonly IReadOnlyList<TextChange> _changes;
+
+ public static INormalizedTextChangeCollection Create(IReadOnlyList<TextChange> changes)
+ {
+ INormalizedTextChangeCollection result = GetTrivialCollection(changes);
+ return result != null ? result : new NormalizedTextChangeCollection(changes);
+ }
+
+ public static INormalizedTextChangeCollection Create(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService,
+ ITextSnapshot before = null, ITextSnapshot after = null)
+ {
+ INormalizedTextChangeCollection result = GetTrivialCollection(changes);
+ return result != null ? result : new NormalizedTextChangeCollection(changes, differenceOptions, textDifferencingService, before, after);
+ }
+
+ private static INormalizedTextChangeCollection GetTrivialCollection(IReadOnlyList<TextChange> changes)
+ {
+ if (changes == null)
+ {
+ throw new ArgumentNullException("changes");
+ }
+
+ if (changes.Count == 0)
+ {
+ return NormalizedTextChangeCollection.Empty;
+ }
+ else if (changes.Count == 1)
+ {
+ TextChange tc = changes[0];
+ if (tc.OldLength + tc.NewLength == 1 && tc.LineBreakBoundaryConditions == LineBreakBoundaryConditions.None &&
+ tc.LineCountDelta == 0)
+ {
+ bool isInsertion = tc.NewLength == 1;
+ char data = isInsertion ? tc.NewText[0] : tc.OldText[0];
+ return new TrivialNormalizedTextChangeCollection(data, isInsertion, tc.OldPosition);
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Construct a normalized version of the given TextChange collection,
+ /// but don't compute minimal edits.
+ /// </summary>
+ /// <param name="changes">List of changes to normalize</param>
+ private NormalizedTextChangeCollection(IReadOnlyList<TextChange> changes)
+ {
+ _changes = Normalize(changes, null, null, null, null);
+ }
+
+ /// <summary>
+ /// Construct a normalized version of the given TextChange collection.
+ /// </summary>
+ /// <param name="changes">List of changes to normalize</param>
+ /// <param name="differenceOptions">The difference options to use for minimal differencing, if any.</param>
+ /// <param name="textDifferencingService">The difference service to use, if differenceOptions were supplied.</param>
+ /// <param name="before">Text snapshot before the change (can be null).</param>
+ /// <param name="after">Text snapshot after the change (can be null).</param>
+ private NormalizedTextChangeCollection(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService,
+ ITextSnapshot before, ITextSnapshot after)
+ {
+ _changes = Normalize(changes, differenceOptions, textDifferencingService, before, after);
+ }
+
+ public bool IncludesLineChanges
+ {
+ get
+ {
+ foreach (ITextChange change in this)
+ {
+ if (change.LineCountDelta != 0)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Normalize a sequence of changes that were all applied consecutively to the same version of a buffer. Positions of the
+ /// normalized changes are adjusted to account for other changes that occur at lower indexes in the
+ /// buffer, and changes are sorted and merged if possible.
+ /// </summary>
+ /// <param name="changes">The changes to normalize.</param>
+ /// <param name="differenceOptions">The options to use for minimal differencing, if any.</param>
+ /// <param name="before">Text snapshot before the change (can be null).</param>
+ /// <param name="after">Text snapshot after the change (can be null).</param>
+ /// <returns>A (possibly empty) list of changes, sorted by Position, with adjacent and overlapping changes combined
+ /// where possible.</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="changes"/> is null.</exception>
+ private static IReadOnlyList<TextChange> Normalize(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService,
+ ITextSnapshot before, ITextSnapshot after)
+ {
+ if (changes.Count == 1 && differenceOptions == null)
+ {
+ // By far the most common path
+ // If we are computing minimal changes, we need to go through the
+ // algorithm anyway, since this change may be split up into many
+ // smaller changes
+ return new TextChange[] { changes[0] };
+ }
+ else if (changes.Count == 0)
+ {
+ return new TextChange[0];
+ }
+
+ TextChange[] work = TextUtilities.StableSort(changes, TextChange.Compare);
+
+ // work is now sorted by increasing Position
+
+ int accumulatedDelta = 0;
+ int a = 0;
+ int b = 1;
+ while (b < work.Length)
+ {
+ // examine a pair of changes and attempt to combine them
+ TextChange aChange = work[a];
+ TextChange bChange = work[b];
+ int gap = bChange.OldPosition - aChange.OldEnd;
+
+ if (gap > 0)
+ {
+ // independent changes
+ aChange.NewPosition = aChange.OldPosition + accumulatedDelta;
+ accumulatedDelta += aChange.Delta;
+ a = b;
+ b = a + 1;
+ }
+ else
+ {
+ // dependent changes. merge all adjacent dependent changes into a single change in one pass,
+ // to avoid expensive pairwise concatenations.
+ //
+ // Use StringRebuilders (which allow strings to be concatenated without creating copies of the strings) to assemble the
+ // pieces.
+ StringRebuilder newRebuilder = aChange._newText;
+ StringRebuilder oldRebuilder = aChange._oldText;
+
+ int aChangeIncrementalDeletions = 0;
+ do
+ {
+ newRebuilder = newRebuilder.Append(bChange._newText);
+
+ if (gap == 0)
+ {
+ // abutting deletions
+ oldRebuilder = oldRebuilder.Append(bChange._oldText);
+ aChangeIncrementalDeletions += bChange.OldLength;
+ aChange.LineBreakBoundaryConditions =
+ (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) |
+ (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline);
+ }
+ else
+ {
+ // overlapping deletions
+ if (aChange.OldEnd + aChangeIncrementalDeletions < bChange.OldEnd)
+ {
+ int overlap = aChange.OldEnd + aChangeIncrementalDeletions - bChange.OldPosition;
+ oldRebuilder = oldRebuilder.Append(bChange._oldText.GetSubText(Span.FromBounds(overlap, bChange._oldText.Length)));
+ aChangeIncrementalDeletions += (bChange.OldLength - overlap);
+ aChange.LineBreakBoundaryConditions =
+ (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) |
+ (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline);
+ }
+ // else bChange deletion subsumed by aChange deletion
+ }
+
+ work[b] = null;
+ b++;
+ if (b == work.Length)
+ {
+ break;
+ }
+ bChange = work[b];
+ gap = bChange.OldPosition - aChange.OldEnd - aChangeIncrementalDeletions;
+ } while (gap <= 0);
+
+ work[a]._oldText = oldRebuilder;
+ work[a]._newText = newRebuilder;
+
+ if (b < work.Length)
+ {
+ aChange.NewPosition = aChange.OldPosition + accumulatedDelta;
+ accumulatedDelta += aChange.Delta;
+ a = b;
+ b = a + 1;
+ }
+ }
+ }
+ // a points to the last surviving change
+ work[a].NewPosition = work[a].OldPosition + accumulatedDelta;
+
+ List<TextChange> result = new List<TextChange>();
+
+ if (differenceOptions.HasValue)
+ {
+ if (textDifferencingService == null)
+ {
+ throw new ArgumentNullException("stringDifferenceUtility");
+ }
+ foreach (TextChange change in work)
+ {
+ if (change == null) continue;
+
+ // Make sure this is a replacement
+ if (change.OldLength == 0 || change.NewLength == 0)
+ {
+ result.Add(change);
+ continue;
+ }
+
+ if (change.OldLength >= TextModelOptions.DiffSizeThreshold ||
+ change.NewLength >= TextModelOptions.DiffSizeThreshold)
+ {
+ change.IsOpaque = true;
+ result.Add(change);
+ continue;
+ // too big to even attempt a diff. This is aimed at the reload-a-giant-file scenario
+ // where OOM during diff is a distinct possibility.
+ }
+
+ // Make sure to turn off IgnoreTrimWhiteSpace, since that doesn't make sense in
+ // the context of a minimal edit
+ StringDifferenceOptions options = new StringDifferenceOptions(differenceOptions.Value);
+ options.IgnoreTrimWhiteSpace = false;
+ IHierarchicalDifferenceCollection diffs;
+
+ if (before != null && after != null)
+ {
+ // Don't materialize the strings when we know the before and after snapshots. They might be really huge and cause OOM.
+ // We will take this path in the file reload case.
+ diffs = textDifferencingService.DiffSnapshotSpans(new SnapshotSpan(before, change.OldSpan),
+ new SnapshotSpan(after, change.NewSpan), options);
+ }
+ else
+ {
+ // We need to evaluate the old and new text for the differencing service
+ string oldText = change.OldText;
+ string newText = change.NewText;
+
+ if (oldText == newText)
+ {
+ // This change simply evaporates. This case occurs frequently in Venus and it is much
+ // better to short circuit it here than to fire up the differencing engine.
+ continue;
+ }
+ diffs = textDifferencingService.DiffStrings(oldText, newText, options);
+ }
+
+ // Keep track of deltas for the "new" position, for sanity check
+ int delta = 0;
+
+ // Add all the changes from the difference collection
+ result.AddRange(GetChangesFromDifferenceCollection(ref delta, change, change._oldText, change._newText, diffs));
+
+ // Sanity check
+ // If delta != 0, then we've constructed asymmetrical insertions and
+ // deletions, which should be impossible
+ Debug.Assert(delta == change.Delta, "Minimal edit delta should be equal to replaced text change's delta.");
+ }
+ }
+ // If we aren't computing minimal changes, then copy over the non-null changes
+ else
+ {
+ foreach (TextChange change in work)
+ {
+ if (change != null)
+ result.Add(change);
+ }
+ }
+
+ return result;
+ }
+
+ private static IList<TextChange> GetChangesFromDifferenceCollection(ref int delta,
+ TextChange originalChange,
+ StringRebuilder oldText,
+ StringRebuilder newText,
+ IHierarchicalDifferenceCollection diffCollection,
+ int leftOffset = 0,
+ int rightOffset = 0)
+ {
+ List<TextChange> changes = new List<TextChange>();
+
+ for (int i = 0; i < diffCollection.Differences.Count; i++)
+ {
+ Difference currentDiff = diffCollection.Differences[i];
+
+ Span leftDiffSpan = Translate(diffCollection.LeftDecomposition.GetSpanInOriginal(currentDiff.Left), leftOffset);
+ Span rightDiffSpan = Translate(diffCollection.RightDecomposition.GetSpanInOriginal(currentDiff.Right), rightOffset);
+
+ // TODO: Since this evaluates differences lazily, we should add something here to *not* compute the next
+ // level of differences if we think it would be too expensive.
+ IHierarchicalDifferenceCollection nextLevelDiffs = diffCollection.GetContainedDifferences(i);
+
+ if (nextLevelDiffs != null)
+ {
+ changes.AddRange(GetChangesFromDifferenceCollection(ref delta, originalChange, oldText, newText, nextLevelDiffs, leftDiffSpan.Start, rightDiffSpan.Start));
+ }
+ else
+ {
+ TextChange minimalChange = new TextChange(originalChange.OldPosition + leftDiffSpan.Start,
+ oldText.GetSubText(leftDiffSpan),
+ newText.GetSubText(rightDiffSpan),
+ ComputeBoundaryConditions(originalChange, oldText, leftDiffSpan));
+
+ minimalChange.NewPosition = originalChange.NewPosition + rightDiffSpan.Start;
+ if (minimalChange.OldLength > 0 && minimalChange.NewLength > 0)
+ {
+ minimalChange.IsOpaque = true;
+ }
+
+ delta += minimalChange.Delta;
+ changes.Add(minimalChange);
+ }
+ }
+
+ return changes;
+ }
+
+
+ private static LineBreakBoundaryConditions ComputeBoundaryConditions(TextChange outerChange, StringRebuilder oldText, Span leftSpan)
+ {
+ LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None;
+ if (leftSpan.Start == 0)
+ {
+ bc = (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn);
+ }
+ else if (oldText[leftSpan.Start - 1] == '\r')
+ {
+ bc = LineBreakBoundaryConditions.PrecedingReturn;
+ }
+ if (leftSpan.End == oldText.Length)
+ {
+ bc |= (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline);
+ }
+ else if (oldText[leftSpan.End] == '\n')
+ {
+ bc |= LineBreakBoundaryConditions.SucceedingNewline;
+ }
+ return bc;
+ }
+
+ private static Span Translate(Span span, int amount)
+ {
+ return new Span(span.Start + amount, span.Length);
+ }
+
+ private static bool PriorTo(ITextChange denormalizedChange, ITextChange normalizedChange, int accumulatedDelta, int accumulatedNormalizedDelta)
+ {
+ // notice that denormalizedChange.OldPosition == denormalizedChange.NewPosition
+ if ((denormalizedChange.OldLength != 0) && (normalizedChange.OldLength != 0))
+ {
+ // both deletions
+ return denormalizedChange.OldPosition <= normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta;
+ }
+ else
+ {
+ return denormalizedChange.OldPosition < normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta;
+ }
+ }
+
+ /// <summary>
+ /// Given a set of changes against a particular snapshot, merge in a list of normalized changes that occurred
+ /// immediately after those changes (as part of the next snapshot) so that the merged changes refer to the
+ /// earlier snapshot.
+ /// </summary>
+ /// <param name="normalizedChanges">The list of changes to be merged.</param>
+ /// <param name="denormChangesWithSentinel">The list of changes into which to merge.</param>
+ public static void Denormalize(INormalizedTextChangeCollection normalizedChanges, List<TextChange> denormChangesWithSentinel)
+ {
+ // denormalizedChangesWithSentinel contains a list of changes that have been denormalized to the origin snapshot
+ // (the New positions in those changes are the same as the Old positions), and also has a sentinel at the end
+ // that has int.MaxValue for its position.
+ // args.Changes contains a list of changes that are normalized with respect to the most recent snapshot, so we know
+ // that they are independent and properly ordered -- thus we can perform a single merge pass against the
+ // denormalized changes.
+
+ int rover = 0;
+ int accumulatedDelta = 0;
+ int accumulatedNormalizedDelta = 0;
+ List<ITextChange> normChanges = new List<ITextChange>(normalizedChanges);
+ for (int n = 0; n < normChanges.Count; ++n)
+ {
+ ITextChange normChange = normChanges[n];
+
+ // 1. skip past all denormalized changes that begin prior to the beginning of the current change.
+
+ while (PriorTo(denormChangesWithSentinel[rover], normChange, accumulatedDelta, accumulatedNormalizedDelta))
+ {
+ accumulatedDelta += denormChangesWithSentinel[rover++].Delta;
+ }
+
+ // 2. normChange will be inserted at [rover], but it may need to be split
+
+ if ((normChange.OldEnd - accumulatedDelta) > denormChangesWithSentinel[rover].OldPosition)
+ {
+ // split required. for example, text at 5..10 was deleted in snapshot 1, and then text at 0..10 was deleted
+ // in snapshot 2; the latter turns into two deletions in terms of snapshot 1: 0..5 and 10..15.
+ int deletionSuffix = (normChange.OldEnd - accumulatedDelta) - denormChangesWithSentinel[rover].OldPosition;
+ int deletionPrefix = normChange.OldLength - deletionSuffix;
+ int normDelta = normChange.NewPosition - normChange.OldPosition;
+ denormChangesWithSentinel.Insert
+ (rover, new TextChange(normChange.OldPosition - accumulatedDelta,
+ TextChange.ChangeOldSubText(normChange, 0, deletionPrefix),
+ TextChange.NewStringRebuilder(normChange),
+ LineBreakBoundaryConditions.None));
+ accumulatedNormalizedDelta += normDelta;
+
+ // the second part remains 'normalized' in case it needs to be split again
+ TextChange splitee = new TextChange(normChange.OldPosition + deletionPrefix,
+ TextChange.ChangeOldSubText(normChange, deletionPrefix, deletionSuffix),
+ StringRebuilder.Empty,
+ LineBreakBoundaryConditions.None);
+ splitee.NewPosition += normDelta;
+ normChanges.Insert(n + 1, splitee);
+ }
+ else
+ {
+ denormChangesWithSentinel.Insert
+ (rover, new TextChange(normChange.OldPosition - accumulatedDelta,
+ TextChange.OldStringRebuilder(normChange),
+ TextChange.NewStringRebuilder(normChange),
+ LineBreakBoundaryConditions.None));
+ accumulatedNormalizedDelta += normChange.Delta;
+ }
+
+ rover++;
+ }
+ }
+
+ int ICollection<ITextChange>.Count => _changes.Count;
+
+ bool ICollection<ITextChange>.IsReadOnly => true;
+
+ ITextChange IList<ITextChange>.this[int index]
+ {
+ get => _changes[index];
+ set => throw new System.NotSupportedException();
+ }
+
+
+ int IList<ITextChange>.IndexOf(ITextChange item)
+ {
+ for (int i = 0; (i < _changes.Count); ++i)
+ {
+ if (item.Equals(_changes[i]))
+ return i;
+ }
+
+ return -1;
+ }
+
+ void IList<ITextChange>.Insert(int index, ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ void IList<ITextChange>.RemoveAt(int index)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ void ICollection<ITextChange>.Add(ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ void ICollection<ITextChange>.Clear()
+ {
+ throw new System.NotSupportedException();
+ }
+
+ bool ICollection<ITextChange>.Contains(ITextChange item)
+ {
+ return ((IList<ITextChange>)this).IndexOf(item) != -1;
+ }
+
+ void ICollection<ITextChange>.CopyTo(ITextChange[] array, int arrayIndex)
+ {
+ for (int i = 0; (i < _changes.Count); ++i)
+ {
+ array[i + arrayIndex] = _changes[i];
+ }
+ }
+
+ bool ICollection<ITextChange>.Remove(ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ IEnumerator<ITextChange> IEnumerable<ITextChange>.GetEnumerator()
+ {
+ return _changes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _changes.GetEnumerator();
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/PersistentSpan.cs b/src/Text/Impl/TextModel/PersistentSpan.cs
new file mode 100644
index 0000000..79f205d
--- /dev/null
+++ b/src/Text/Impl/TextModel/PersistentSpan.cs
@@ -0,0 +1,216 @@
+//
+// 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 Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Utilities;
+ using System;
+ using System.Diagnostics;
+
+ internal sealed class PersistentSpan : IPersistentSpan
+ {
+ #region members
+ private PersistentSpanFactory _factory;
+
+ private ITrackingSpan _span; //null for spans on closed documents or disposed spans
+ private ITextDocument _document; //null for spans on closed documents or disposed spans
+ private string _filePath; //null for spans on opened documents or disposed spans
+ private int _startLine; //these parameters are valid whether or not the document is open (but _start*,_end* may be stale).
+ private int _startIndex;
+ private int _endLine;
+ private int _endIndex;
+ private Span _nonTrackingSpan;
+ private bool _useLineIndex;
+
+ private readonly SpanTrackingMode _trackingMode;
+ #endregion
+
+ internal PersistentSpan(ITextDocument document, SnapshotSpan span, SpanTrackingMode trackingMode, PersistentSpanFactory factory)
+ {
+ //Arguments verified in factory
+ _document = document;
+
+ _span = span.Snapshot.CreateTrackingSpan(span, trackingMode);
+ _trackingMode = trackingMode;
+
+ _factory = factory;
+ }
+
+ internal PersistentSpan(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode, PersistentSpanFactory factory)
+ {
+ //Arguments verified in factory
+ _filePath = filePath;
+
+ _useLineIndex = true;
+ _startLine = startLine;
+ _startIndex = startIndex;
+ _endLine = endLine;
+ _endIndex = endIndex;
+
+ _trackingMode = trackingMode;
+
+ _factory = factory;
+ }
+
+ internal PersistentSpan(string filePath, Span span, SpanTrackingMode trackingMode, PersistentSpanFactory factory)
+ {
+ //Arguments verified in factory
+ _filePath = filePath;
+
+ _useLineIndex = false;
+ _nonTrackingSpan = span;
+
+ _trackingMode = trackingMode;
+
+ _factory = factory;
+ }
+
+ #region IPersistentSpan members
+ public bool IsDocumentOpen { get { return _document != null; } }
+
+ public ITextDocument Document { get { return _document; } }
+
+ public ITrackingSpan Span { get { return _span; } }
+
+ public string FilePath
+ {
+ get
+ {
+ return (_document != null) ? _document.FilePath : _filePath;
+ }
+ }
+
+ public bool TryGetStartLineIndex(out int startLine, out int startIndex)
+ {
+ if ((_document == null) && (_filePath == null))
+ throw new ObjectDisposedException("PersistentSpan");
+
+ if (_span != null)
+ this.UpdateStartEnd();
+
+ startLine = _startLine;
+ startIndex = _startIndex;
+
+ return ((_span != null) || _useLineIndex);
+ }
+
+ public bool TryGetEndLineIndex(out int endLine, out int endIndex)
+ {
+ if ((_document == null) && (_filePath == null))
+ throw new ObjectDisposedException("PersistentSpan");
+
+ if (_span != null)
+ this.UpdateStartEnd();
+
+ endLine = _endLine;
+ endIndex = _endIndex;
+ return ((_span != null) || _useLineIndex);
+ }
+
+ public bool TryGetSpan(out Span span)
+ {
+ if ((_document == null) && (_filePath == null))
+ throw new ObjectDisposedException("PersistentSpan");
+
+ if (_span != null)
+ this.UpdateStartEnd();
+
+ span = _nonTrackingSpan;
+ return ((_span != null) || !_useLineIndex);
+ }
+ #endregion
+
+ #region IDisposable members
+ public void Dispose()
+ {
+ if ((_document != null) || (_filePath != null))
+ {
+ _factory.Delete(this);
+
+ _span = null;
+ _document = null;
+ _filePath = null;
+ }
+ }
+ #endregion
+
+ #region private helpers
+ internal void DocumentClosed()
+ {
+ this.UpdateStartEnd();
+
+ //We set this to false when the document is closed because we have an accurate line/index and that is more stable
+ //than a simple offset.
+ _useLineIndex = true;
+ _nonTrackingSpan = new Span(0, 0);
+
+ _filePath = _document.FilePath;
+ _document = null;
+ _span = null;
+ }
+
+ internal void DocumentReopened(ITextDocument document)
+ {
+ _document = document;
+
+ ITextSnapshot snapshot = document.TextBuffer.CurrentSnapshot;
+
+ SnapshotPoint start;
+ SnapshotPoint end;
+ if (_useLineIndex)
+ {
+ start = PersistentSpan.LineIndexToSnapshotPoint(_startLine, _startIndex, snapshot);
+ end = PersistentSpan.LineIndexToSnapshotPoint(_endLine, _endIndex, snapshot);
+
+ if (end < start)
+ {
+ //Guard against the case where _start & _end are something like (100,2) & (101, 1).
+ //Those points would pass the argument validation (since _endLine > _startLine) but
+ //would cause problems if the document has only 5 lines since they would map to
+ //(5, 2) & (5, 1).
+ end = start;
+ }
+ }
+ else
+ {
+ start = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.Start, snapshot.Length));
+ end = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.End, snapshot.Length));
+ }
+
+ _span = snapshot.CreateTrackingSpan(new SnapshotSpan(start, end), _trackingMode);
+
+ _filePath = null;
+ }
+
+ private void UpdateStartEnd()
+ {
+ SnapshotSpan span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot);
+
+ _nonTrackingSpan = span;
+
+ PersistentSpan.SnapshotPointToLineIndex(span.Start, out _startLine, out _startIndex);
+ PersistentSpan.SnapshotPointToLineIndex(span.End, out _endLine, out _endIndex);
+ }
+
+ private static void SnapshotPointToLineIndex(SnapshotPoint p, out int line, out int index)
+ {
+ ITextSnapshotLine l = p.GetContainingLine();
+
+ line = l.LineNumber;
+ index = Math.Min(l.Length, p - l.Start);
+ }
+
+ internal static SnapshotPoint LineIndexToSnapshotPoint(int line, int index, ITextSnapshot snapshot)
+ {
+ ITextSnapshotLine l = snapshot.GetLineFromLineNumber(Math.Min(line, snapshot.LineCount - 1));
+
+ return l.Start + Math.Min(index, l.Length);
+ }
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/PersistentSpanFactory.cs b/src/Text/Impl/TextModel/PersistentSpanFactory.cs
new file mode 100644
index 0000000..6ea3a3d
--- /dev/null
+++ b/src/Text/Impl/TextModel/PersistentSpanFactory.cs
@@ -0,0 +1,295 @@
+//
+// 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 Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.IO;
+
+ [Export(typeof(IPersistentSpanFactory))]
+ internal class PersistentSpanFactory : IPersistentSpanFactory
+ {
+ [Import]
+ internal ITextDocumentFactoryService TextDocumentFactoryService;
+
+ private readonly Dictionary<object, FrugalList<PersistentSpan>> _spansOnDocuments = new Dictionary<object, FrugalList<PersistentSpan>>(); //Used for lock
+
+ private bool _eventsHooked;
+
+ #region IPersistentSpanFactory members
+ public bool CanCreate(ITextBuffer buffer)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ITextDocument document;
+ return this.TextDocumentFactoryService.TryGetTextDocument(buffer, out document);
+ }
+
+ public IPersistentSpan Create(SnapshotSpan span, SpanTrackingMode trackingMode)
+ {
+ ITextDocument document;
+ if (this.TextDocumentFactoryService.TryGetTextDocument(span.Snapshot.TextBuffer, out document))
+ {
+ PersistentSpan persistentSpan = new PersistentSpan(document, span, trackingMode, this);
+
+ this.AddSpan(document, persistentSpan);
+
+ return persistentSpan;
+ }
+
+ return null;
+ }
+
+ public IPersistentSpan Create(ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode)
+ {
+ ITextDocument document;
+ if (this.TextDocumentFactoryService.TryGetTextDocument(snapshot.TextBuffer, out document))
+ {
+ var start = PersistentSpan.LineIndexToSnapshotPoint(startLine, startIndex, snapshot);
+ var end = PersistentSpan.LineIndexToSnapshotPoint(endLine, endIndex, snapshot);
+ if (end < start)
+ {
+ end = start;
+ }
+
+ PersistentSpan persistentSpan = new PersistentSpan(document, new SnapshotSpan(start, end), trackingMode, this);
+
+ this.AddSpan(document, persistentSpan);
+
+ return persistentSpan;
+ }
+
+ return null;
+ }
+
+ public IPersistentSpan Create(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException("filePath");
+ }
+ if (startLine < 0)
+ {
+ throw new ArgumentOutOfRangeException("startLine", "Must be non-negative.");
+ }
+ if (startIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException("startIndex", "Must be non-negative.");
+ }
+ if (endLine < startLine)
+ {
+ throw new ArgumentOutOfRangeException("endLine", "Must be >= startLine.");
+ }
+ if ((endIndex < 0) || ((startLine == endLine) && (endIndex < startIndex)))
+ {
+ throw new ArgumentOutOfRangeException("endIndex", "Must be non-negative and (endLine,endIndex) may not be before (startLine,startIndex).");
+ }
+ if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative)))
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+
+ PersistentSpan persistentSpan = new PersistentSpan(filePath, startLine, startIndex, endLine, endIndex, trackingMode, this);
+
+ this.AddSpan(new FileNameKey(filePath), persistentSpan);
+
+ return persistentSpan;
+ }
+
+ public IPersistentSpan Create(string filePath, Span span, SpanTrackingMode trackingMode)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException("filePath");
+ }
+ if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative)))
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+
+ PersistentSpan persistentSpan = new PersistentSpan(filePath, span, trackingMode, this);
+
+ this.AddSpan(new FileNameKey(filePath), persistentSpan);
+
+ return persistentSpan;
+ }
+ #endregion
+
+ internal bool IsEmpty { get { return _spansOnDocuments.Count == 0; } } //For unit tests
+
+ private void AddSpan(object key, PersistentSpan persistentSpan)
+ {
+ lock (_spansOnDocuments)
+ {
+ FrugalList<PersistentSpan> spans;
+ if (!_spansOnDocuments.TryGetValue(key, out spans))
+ {
+ this.EnsureEventsHooked();
+
+ spans = new FrugalList<PersistentSpan>();
+ _spansOnDocuments.Add(key, spans);
+ }
+
+ spans.Add(persistentSpan);
+ }
+ }
+
+ private void EnsureEventsHooked()
+ {
+ if (!_eventsHooked)
+ {
+ _eventsHooked = true;
+
+ this.TextDocumentFactoryService.TextDocumentCreated += OnTextDocumentCreated;
+ this.TextDocumentFactoryService.TextDocumentDisposed += OnTextDocumentDisposed;
+ }
+ }
+
+ private void OnTextDocumentCreated(object sender, TextDocumentEventArgs e)
+ {
+ var path = new FileNameKey(e.TextDocument.FilePath);
+ FrugalList<PersistentSpan> spans;
+ lock (_spansOnDocuments)
+ {
+ if (_spansOnDocuments.TryGetValue(path, out spans))
+ {
+ foreach (var span in spans)
+ {
+ span.DocumentReopened(e.TextDocument);
+ }
+
+ _spansOnDocuments.Remove(path);
+ _spansOnDocuments.Add(e.TextDocument, spans);
+ }
+ }
+ }
+
+ private void OnTextDocumentDisposed(object sender, TextDocumentEventArgs e)
+ {
+ FrugalList<PersistentSpan> spans;
+ lock (_spansOnDocuments)
+ {
+ if (_spansOnDocuments.TryGetValue(e.TextDocument, out spans))
+ {
+ foreach (var span in spans)
+ {
+ span.DocumentClosed();
+ }
+
+ _spansOnDocuments.Remove(e.TextDocument);
+
+ var path = new FileNameKey(e.TextDocument.FilePath);
+ FrugalList<PersistentSpan> existingSpansOnPath;
+ if (_spansOnDocuments.TryGetValue(path, out existingSpansOnPath))
+ {
+ //Handle (badly) the case where a document is renamed to an existing closed document & then closed.
+ existingSpansOnPath.AddRange(spans);
+ }
+ else
+ {
+ _spansOnDocuments.Add(path, spans);
+ }
+ }
+ }
+ }
+
+ internal void Delete(PersistentSpan span)
+ {
+ lock (_spansOnDocuments)
+ {
+ ITextDocument document = span.Document;
+ if (document != null)
+ {
+ FrugalList<PersistentSpan> spans;
+ if (_spansOnDocuments.TryGetValue(document, out spans))
+ {
+ spans.Remove(span);
+
+ if (spans.Count == 0)
+ {
+ //Last one ... remove all references to document.
+ _spansOnDocuments.Remove(document);
+ }
+ }
+ else
+ {
+ Debug.Fail("There should have been an entry in SpanOnDocuments.");
+ }
+ }
+ else
+ {
+ var path = new FileNameKey(span.FilePath);
+ FrugalList<PersistentSpan> spans;
+ if (_spansOnDocuments.TryGetValue(path, out spans))
+ {
+ spans.Remove(span);
+
+ if (spans.Count == 0)
+ {
+ //Last one ... remove all references to path.
+ _spansOnDocuments.Remove(path);
+ }
+ }
+ else
+ {
+ Debug.Fail("There should have been an entry in SpanOnDocuments.");
+ }
+ }
+ }
+ }
+
+ private class FileNameKey
+ {
+ private readonly string _fileName;
+ private readonly int _hashCode;
+
+ public FileNameKey(string fileName)
+ {
+ //Gracefully catch errors getting the full path (which can happen if the file name is on a protected share).
+ try
+ {
+ _fileName = Path.GetFullPath(fileName);
+ }
+ catch
+ {
+ //This shouldn't happen (we are generally passed names associated with documents that we are expecting to open so
+ //we should have access). If we fail, we will, at worst not get the same underlying document when people create
+ //persistent spans using unnormalized names.
+ _fileName = fileName;
+ }
+
+ _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_fileName);
+ }
+
+ //Override equality and hash code
+ public override int GetHashCode()
+ {
+ return _hashCode;
+ }
+
+ public override bool Equals(object obj)
+ {
+ var other = obj as FileNameKey;
+ return (other != null) && string.Equals(_fileName, other._fileName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override string ToString()
+ {
+ return _fileName;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs
new file mode 100644
index 0000000..5dbdfc3
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs
@@ -0,0 +1,228 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Text;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using System.Collections.ObjectModel;
+
+ internal abstract class BaseProjectionBuffer : BaseBuffer, IProjectionBufferBase
+ {
+ #region State and construction
+ protected internal readonly IProjectionEditResolver resolver;
+ protected bool editApplicationInProgress;
+ protected List<TextContentChangedEventArgs> pendingContentChangedEventArgs = new List<TextContentChangedEventArgs>();
+
+ protected BaseProjectionBuffer(IProjectionEditResolver resolver, IContentType contentType, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations)
+ : base(contentType, 0, textDifferencingService, guardedOperations)
+ {
+ this.resolver = resolver; // null is OK
+ }
+ #endregion
+
+ #region Source buffer event handling
+ internal abstract void OnSourceTextChanged(object sender, TextContentChangedEventArgs e);
+
+ internal virtual void OnSourceBufferReadOnlyRegionsChanged(object sender, SnapshotSpanEventArgs e)
+ {
+ NormalizedSpanCollection mappedAffectedSpans = new NormalizedSpanCollection(this.CurrentBaseSnapshot.MapFromSourceSnapshot(e.Span));
+
+ if (mappedAffectedSpans.Count > 0)
+ {
+ ITextEventRaiser raiser =
+ new ReadOnlyRegionsChangedEventRaiser(new SnapshotSpan(this.currentSnapshot,
+ Span.FromBounds(mappedAffectedSpans[0].Start,
+ mappedAffectedSpans[mappedAffectedSpans.Count - 1].End)));
+
+ this.group.BeginEdit();
+ this.group.EnqueueEvents(raiser, this);
+ this.group.FinishEdit();
+ }
+ }
+
+ internal void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e)
+ {
+ TextContentChangedEventArgs args = new TextContentChangedEventArgs(e.Before, e.After, EditOptions.None, e.EditTag);
+ this.pendingContentChangedEventArgs.Add(args);
+ this.group.ScheduleIndependentEdit(this);
+ }
+ #endregion
+
+ #region Editing Shortcuts
+ public new IProjectionSnapshot Insert(int position, string text)
+ {
+ return (IProjectionSnapshot)base.Insert(position, text);
+ }
+
+ public new IProjectionSnapshot Delete(Span deleteSpan)
+ {
+ return (IProjectionSnapshot)base.Delete(deleteSpan);
+ }
+
+ public new IProjectionSnapshot Replace(Span replaceSpan, string replaceWith)
+ {
+ return (IProjectionSnapshot)base.Replace(replaceSpan, replaceWith);
+ }
+ #endregion
+
+ #region Read Only Region support
+ protected internal override bool IsReadOnlyImplementation(int position, bool isEdit)
+ {
+ if (this.CurrentBaseSnapshot.SpanCount == 0)
+ {
+ throw new InvalidOperationException();
+ }
+ if (base.IsReadOnlyImplementation(position, isEdit))
+ {
+ return true;
+ }
+ return AreBaseBuffersReadOnly(position, isEdit);
+ }
+
+ private bool AreBaseBuffersReadOnly(int position, bool isEdit)
+ {
+ // a position on a seam between two or more source spans is readonly only
+ // if that position is readonly in all of the source buffers.
+ ReadOnlyCollection<SnapshotPoint> snapPoints = this.CurrentBaseSnapshot.MapInsertionPointToSourceSnapshots(position, null);
+ foreach (SnapshotPoint snapPoint in snapPoints)
+ {
+ BaseBuffer baseBuffer = (BaseBuffer)snapPoint.Snapshot.TextBuffer;
+ if (!baseBuffer.IsReadOnlyImplementation(snapPoint.Position, isEdit))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ protected internal override bool IsReadOnlyImplementation(Span span, bool isEdit)
+ {
+ if (this.CurrentSnapshot.SpanCount == 0)
+ {
+ throw new InvalidOperationException();
+ }
+ if (base.IsReadOnlyImplementation(span, isEdit))
+ {
+ return true;
+ }
+ return AreBaseBuffersReadOnly(span, isEdit);
+ }
+
+ public bool AreBaseBuffersReadOnly(Span span, bool isEdit)
+ {
+ if (span.Length == 0)
+ {
+ // treat like an insertion!
+ return AreBaseBuffersReadOnly(span.Start, isEdit);
+ }
+ else
+ {
+ IList<SnapshotSpan> snapSpans = this.CurrentBaseSnapshot.MapToSourceSnapshots(span);
+ foreach (SnapshotSpan snapSpan in snapSpans)
+ {
+ BaseBuffer baseBuffer = (BaseBuffer)snapSpan.Snapshot.TextBuffer;
+ if (baseBuffer.IsReadOnlyImplementation(snapSpan, isEdit))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ protected internal override NormalizedSpanCollection GetReadOnlyExtentsImplementation(Span span)
+ {
+ // TODO: make something other than dead slow
+
+ FrugalList<Span> result = new FrugalList<Span>(base.GetReadOnlyExtentsImplementation(span));
+
+ IList<SnapshotSpan> restrictionSpans = this.CurrentBaseSnapshot.MapToSourceSnapshotsForRead(span);
+ foreach (SnapshotSpan restrictionSpan in restrictionSpans)
+ {
+ SnapshotSpan? overlapSpan = (restrictionSpan.Span == span) ? restrictionSpan : restrictionSpan.Overlap(span);
+ if (overlapSpan.HasValue)
+ {
+ BaseBuffer baseBuffer = (BaseBuffer)restrictionSpan.Snapshot.TextBuffer;
+ NormalizedSpanCollection sourceExtents = baseBuffer.GetReadOnlyExtents(overlapSpan.Value);
+ foreach (Span sourceExtent in sourceExtents)
+ {
+ result.AddRange(this.CurrentBaseSnapshot.MapFromSourceSnapshot(new SnapshotSpan(restrictionSpan.Snapshot, sourceExtent)));
+ }
+ }
+ }
+
+ return new NormalizedSpanCollection(result);
+ }
+ #endregion
+
+ #region Abstract members
+ protected abstract BaseProjectionSnapshot CurrentBaseSnapshot { get; }
+
+ public override abstract ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag);
+
+ protected internal override abstract ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag);
+
+ protected override abstract BaseSnapshot TakeSnapshot();
+
+ public new abstract IProjectionSnapshot CurrentSnapshot { get; }
+
+ public abstract IList<ITextBuffer> SourceBuffers { get; }
+
+ public abstract BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag);
+ #endregion
+
+ #region Debug support
+ [Conditional("_DEBUG")]
+ protected void DumpPendingChanges(List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges)
+ {
+ if (BufferGroup.Tracing)
+ {
+ StringBuilder sb = new StringBuilder(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Pending Changes in {0}\r\n", TextUtilities.GetTag(this)));
+ foreach (var p in pendingSourceChanges)
+ {
+ sb.AppendLine(TextUtilities.GetTag(p.Item1));
+ for (int c = 0; c < p.Item2.Count - 1; ++c) // don't display sentinel
+ {
+ sb.AppendLine(p.Item2[c].ToString());
+ }
+ }
+ sb.AppendLine("");
+ Debug.Write(sb.ToString());
+ }
+ }
+
+ [Conditional("_DEBUG")]
+ protected void DumpPendingContentChangedEventArgs()
+ {
+ if (BufferGroup.Tracing)
+ {
+ StringBuilder sb = new StringBuilder(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Pending Change Events in {0}\r\n", TextUtilities.GetTag(this)));
+ foreach (var args in this.pendingContentChangedEventArgs)
+ {
+ sb.Append(TextUtilities.GetTag(args.Before.TextBuffer));
+ sb.Append(" V");
+ sb.AppendLine(args.After.Version.VersionNumber.ToString());
+ foreach (var change in args.Changes)
+ {
+ sb.AppendLine(change.ToString());
+ }
+ }
+ sb.AppendLine("");
+ Debug.Write(sb.ToString());
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs b/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs
new file mode 100644
index 0000000..3f2a46a
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs
@@ -0,0 +1,73 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Text;
+ using System.Threading;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+
+ internal abstract class BaseProjectionSnapshot : BaseSnapshot, IProjectionSnapshot2
+ {
+ #region State and Construction
+ protected int totalLength = 0;
+ protected int totalLineCount = 1;
+
+ protected BaseProjectionSnapshot(ITextVersion2 version, StringRebuilder builder)
+ : base(version, builder)
+ {
+ }
+ #endregion
+
+ public new abstract IProjectionBufferBase TextBuffer { get; }
+
+ public ReadOnlyCollection<SnapshotPoint> MapToSourceSnapshots(int position)
+ {
+ return MapInsertionPointToSourceSnapshots(position, null);
+ }
+
+ #region Abstract Members
+ /// <summary>
+ /// Given the position of a pure insertion (not a replacement), return the list of source points at which the inserted text
+ /// can be placed. This list has length greater than one only when the insertion point is on the seam of two or more source
+ /// spans.
+ /// </summary>
+ /// <param name="position">The position of the insertion into the projection buffer.</param>
+ /// <param name="excludedBuffer">Buffer to be ignored by virtue of its being a readonly literal buffer.</param>
+ internal abstract ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer);
+
+ /// <summary>
+ /// Given the span of text to be deleted in a Replace operation, return the list of source spans to which it maps. Include any
+ /// zero-length spans either on the boundaries or in the middle of the replacement span; the idea is both map to the deleted
+ /// text and return the list of positions across which the inserted text can be placed.
+ /// </summary>
+ /// <param name="replacementSpan">The span of text to be replaced.</param>
+ /// <param name="excludedBuffer">Buffer to be ignored by virtue of its being a readonly literal buffer; only zero-length spans are possible
+ /// in this buffer.</param>
+ internal abstract ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer);
+
+ public abstract int SpanCount { get; }
+ public abstract ReadOnlyCollection<ITextSnapshot> SourceSnapshots { get; }
+ public abstract ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer);
+ public abstract ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer targetBuffer);
+ public abstract ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match);
+ public abstract ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count);
+ public abstract ReadOnlyCollection<SnapshotSpan> GetSourceSpans();
+
+ public abstract SnapshotPoint MapToSourceSnapshot(int position);
+ public abstract SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity);
+ public abstract SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint point, PositionAffinity affinity);
+ public abstract ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span);
+ public abstract ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span);
+ public abstract ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan span);
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/BufferGraph.cs b/src/Text/Impl/TextModel/Projection/BufferGraph.cs
new file mode 100644
index 0000000..87f42e8
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/BufferGraph.cs
@@ -0,0 +1,773 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Strings = Microsoft.VisualStudio.Text.Implementation.Strings;
+
+ partial class BufferGraph : IBufferGraph
+ {
+ #region Private members
+ private readonly ITextBuffer topBuffer;
+ private readonly GuardedOperations guardedOperations;
+ internal Dictionary<ITextBuffer, FrugalList<IProjectionBufferBase>> importingProjectionBufferMap = new Dictionary<ITextBuffer, FrugalList<IProjectionBufferBase>>();
+ internal List<WeakEventHookForBufferGraph> eventHooks = new List<WeakEventHookForBufferGraph>();
+ #endregion
+
+ #region Construction
+ public BufferGraph(ITextBuffer topBuffer, GuardedOperations guardedOperations)
+ {
+ if (topBuffer == null)
+ {
+ throw new ArgumentNullException("topBuffer");
+ }
+ if (guardedOperations == null)
+ {
+ throw new ArgumentNullException("guardedOperations");
+ }
+
+ this.topBuffer = topBuffer;
+ this.guardedOperations = guardedOperations;
+
+ this.importingProjectionBufferMap.Add(topBuffer, null);
+ // The top buffer has no targets, but it is put here so membership in this map can be used uniformly
+ // to determine whether a buffer is in the buffer graph
+
+ // Subscribe to content type changed events on the toplevel buffer
+ this.eventHooks.Add(new WeakEventHookForBufferGraph(this, topBuffer));
+
+ IProjectionBufferBase projectionBufferBase = topBuffer as IProjectionBufferBase;
+ if (projectionBufferBase != null)
+ {
+ IList<ITextBuffer> sourceBuffers = projectionBufferBase.SourceBuffers;
+ FrugalList<ITextBuffer> dontCare = new FrugalList<ITextBuffer>();
+ foreach (ITextBuffer sourceBuffer in sourceBuffers)
+ {
+ AddSourceBuffer(projectionBufferBase, sourceBuffer, dontCare);
+ }
+ }
+ }
+ #endregion
+
+ #region Buffers
+ public ITextBuffer TopBuffer
+ {
+ get { return this.topBuffer; }
+ }
+
+ public Collection<ITextBuffer> GetTextBuffers(Predicate<ITextBuffer> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ FrugalList<ITextBuffer> buffers = new FrugalList<ITextBuffer>();
+ foreach (ITextBuffer buffer in this.importingProjectionBufferMap.Keys)
+ {
+ if (match(buffer))
+ {
+ buffers.Add(buffer);
+ }
+ }
+ return new Collection<ITextBuffer>(buffers);
+ }
+ #endregion
+
+ #region Mapping Point/Span Factories
+ public IMappingPoint CreateMappingPoint(SnapshotPoint point, PointTrackingMode trackingMode)
+ {
+ return new MappingPoint(point, trackingMode, this);
+ }
+
+ public IMappingSpan CreateMappingSpan(SnapshotSpan span, SpanTrackingMode trackingMode)
+ {
+ return new MappingSpan(span, trackingMode, this);
+ }
+ #endregion
+
+ #region Point Mapping
+ public SnapshotPoint? MapDownToFirstMatch(SnapshotPoint position, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity)
+ {
+ if (position.Snapshot == null)
+ {
+ throw new ArgumentNullException("position");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+ if (!this.importingProjectionBufferMap.ContainsKey(position.Snapshot.TextBuffer))
+ {
+ return null;
+ }
+
+ ITextBuffer currentBuffer = position.Snapshot.TextBuffer;
+ ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot;
+ int currentPosition = position.TranslateTo(currentSnapshot, trackingMode).Position;
+ while (!match(currentSnapshot))
+ {
+ IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase;
+ if (projBuffer == null)
+ {
+ return null;
+ }
+ IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot;
+ if (projSnap.SourceSnapshots.Count == 0)
+ {
+ return null;
+ }
+ SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition, affinity);
+ currentPosition = currentPoint.Position;
+ currentSnapshot = currentPoint.Snapshot;
+ currentBuffer = currentSnapshot.TextBuffer;
+ }
+ return new SnapshotPoint(currentSnapshot, currentPosition);
+ }
+
+ public SnapshotPoint? MapDownToInsertionPoint(SnapshotPoint position, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match)
+ {
+ if (position.Snapshot == null)
+ {
+ throw new ArgumentNullException("position");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+
+ ITextBuffer currentBuffer = position.Snapshot.TextBuffer;
+ int currentPosition = position.TranslateTo(currentBuffer.CurrentSnapshot, trackingMode);
+ ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot;
+ while (!match(currentSnapshot))
+ {
+ IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase;
+ if (projBuffer == null)
+ {
+ return null;
+ }
+ IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot;
+ if (projSnap.SourceSnapshots.Count == 0)
+ {
+ return null;
+ }
+ SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition);
+ currentPosition = currentPoint.Position;
+ currentSnapshot = currentPoint.Snapshot;
+ currentBuffer = currentSnapshot.TextBuffer;
+ }
+ return new SnapshotPoint(currentSnapshot, currentPosition);
+ }
+
+ public SnapshotPoint? MapDownToBuffer(SnapshotPoint position, PointTrackingMode trackingMode, ITextBuffer targetBuffer, PositionAffinity affinity)
+ {
+ if (position.Snapshot == null)
+ {
+ throw new ArgumentNullException("position");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (targetBuffer == null)
+ {
+ throw new ArgumentNullException("targetBuffer");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+
+ ITextBuffer currentBuffer = position.Snapshot.TextBuffer;
+ ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot;
+ int currentPosition = position.TranslateTo(currentSnapshot, trackingMode).Position;
+
+ while (currentBuffer != targetBuffer)
+ {
+ IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase;
+ if (projBuffer == null)
+ {
+ return null;
+ }
+ IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot;
+ if (projSnap.SourceSnapshots.Count == 0)
+ {
+ return null;
+ }
+ SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition, affinity);
+ currentPosition = currentPoint.Position;
+ currentSnapshot = currentPoint.Snapshot;
+ currentBuffer = currentSnapshot.TextBuffer;
+ }
+
+ return new SnapshotPoint(currentSnapshot, currentPosition);
+ }
+
+ public SnapshotPoint? MapDownToSnapshot(SnapshotPoint position, PointTrackingMode trackingMode, ITextSnapshot targetSnapshot, PositionAffinity affinity)
+ {
+ if (targetSnapshot == null)
+ {
+ throw new ArgumentNullException("targetSnapshot");
+ }
+
+ SnapshotPoint? result = MapDownToBuffer(position, trackingMode, targetSnapshot.TextBuffer, affinity);
+ if (result.HasValue && (result.Value.Snapshot != targetSnapshot))
+ {
+ result = result.Value.TranslateTo(targetSnapshot, trackingMode);
+ }
+
+ return result;
+ }
+
+ public SnapshotPoint? MapUpToBuffer(SnapshotPoint point, PointTrackingMode trackingMode, PositionAffinity affinity, ITextBuffer targetBuffer)
+ {
+ return CheckedMapUpToBuffer(point, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer), affinity);
+ }
+
+
+ public SnapshotPoint? MapUpToSnapshot(SnapshotPoint position, PointTrackingMode trackingMode, PositionAffinity affinity, ITextSnapshot targetSnapshot)
+ {
+ if (targetSnapshot == null)
+ {
+ throw new ArgumentNullException("targetSnapshot");
+ }
+
+ SnapshotPoint? result = MapUpToBuffer(position, trackingMode, affinity, targetSnapshot.TextBuffer);
+ if (result.HasValue && (result.Value.Snapshot != targetSnapshot))
+ {
+ result = result.Value.TranslateTo(targetSnapshot, trackingMode);
+ }
+
+ return result;
+ }
+
+ public SnapshotPoint? MapUpToFirstMatch(SnapshotPoint point, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ return CheckedMapUpToBuffer(point, trackingMode, match, affinity);
+ }
+
+ private SnapshotPoint? CheckedMapUpToBuffer(SnapshotPoint point, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity)
+ {
+ if (point.Snapshot == null)
+ {
+ throw new ArgumentNullException("point");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+
+ if (!this.importingProjectionBufferMap.ContainsKey(point.Snapshot.TextBuffer))
+ {
+ return null;
+ }
+ else
+ {
+ SnapshotPoint currentPoint = point.TranslateTo(point.Snapshot.TextBuffer.CurrentSnapshot, trackingMode);
+ return MapUpToBufferGuts(currentPoint, affinity, match);
+ }
+ }
+
+ private SnapshotPoint? MapUpToBufferGuts(SnapshotPoint currentPoint, PositionAffinity affinity, Predicate<ITextSnapshot> match)
+ {
+ if (match(currentPoint.Snapshot))
+ {
+ return currentPoint;
+ }
+ FrugalList<IProjectionBufferBase> targetBuffers = this.importingProjectionBufferMap[currentPoint.Snapshot.TextBuffer];
+ if (targetBuffers != null) // targetBuffers will be null if we fell off the top
+ {
+ foreach (IProjectionBufferBase projBuffer in targetBuffers)
+ {
+ SnapshotPoint? position = projBuffer.CurrentSnapshot.MapFromSourceSnapshot(currentPoint, affinity);
+ if (position.HasValue)
+ {
+ position = MapUpToBufferGuts(position.Value, affinity, match);
+ if (position.HasValue)
+ {
+ return position;
+ }
+ }
+ }
+ }
+ return null;
+ }
+ #endregion
+
+ #region Span Mapping
+ public NormalizedSnapshotSpanCollection MapDownToFirstMatch(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match)
+ {
+ if (span.Snapshot == null)
+ {
+ throw new ArgumentNullException("span");
+ }
+ if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+
+ if (!this.importingProjectionBufferMap.ContainsKey(span.Snapshot.TextBuffer))
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+
+ ITextBuffer currentBuffer = span.Snapshot.TextBuffer;
+ SnapshotSpan currentTopSpan = span.TranslateTo(currentBuffer.CurrentSnapshot, trackingMode);
+
+ if (match(currentBuffer.CurrentSnapshot))
+ {
+ return new NormalizedSnapshotSpanCollection(currentTopSpan);
+ }
+ else if (!(currentBuffer is IProjectionBufferBase))
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+ else
+ {
+ FrugalList<Span> targetSpans = new FrugalList<Span>();
+ FrugalList<SnapshotSpan> spans = new FrugalList<SnapshotSpan>() { currentTopSpan };
+ ITextSnapshot chosenSnapshot = null;
+ do
+ {
+ spans = MapDownOneLevel(spans, match, ref chosenSnapshot, ref targetSpans);
+ } while (spans.Count > 0);
+ return chosenSnapshot == null ? NormalizedSnapshotSpanCollection.Empty : new NormalizedSnapshotSpanCollection(chosenSnapshot, targetSpans);
+ }
+ }
+
+ public NormalizedSnapshotSpanCollection MapDownToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, ITextBuffer targetBuffer)
+ {
+ if (targetBuffer == null)
+ {
+ throw new ArgumentNullException("targetBuffer");
+ }
+
+ if (!this.importingProjectionBufferMap.ContainsKey(targetBuffer))
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+ else
+ {
+ return MapDownToFirstMatch(span, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer));
+ }
+ }
+
+ public NormalizedSnapshotSpanCollection MapDownToSnapshot(SnapshotSpan span, SpanTrackingMode trackingMode, ITextSnapshot targetSnapshot)
+ {
+ if (targetSnapshot == null)
+ {
+ throw new ArgumentNullException("targetSnapshot");
+ }
+
+ NormalizedSnapshotSpanCollection results = MapDownToBuffer(span, trackingMode, targetSnapshot.TextBuffer);
+ if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot))
+ {
+ FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan s in results)
+ {
+ translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode));
+ }
+
+ results = new NormalizedSnapshotSpanCollection(translatedSpans);
+ }
+
+ return results;
+ }
+
+ public NormalizedSnapshotSpanCollection MapUpToSnapshot(SnapshotSpan span, SpanTrackingMode trackingMode, ITextSnapshot targetSnapshot)
+ {
+ if (targetSnapshot == null)
+ {
+ throw new ArgumentNullException("targetSnapshot");
+ }
+
+ NormalizedSnapshotSpanCollection results = MapUpToBuffer(span, trackingMode, targetSnapshot.TextBuffer);
+ if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot))
+ {
+ FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan s in results)
+ {
+ translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode));
+ }
+
+ results = new NormalizedSnapshotSpanCollection(translatedSpans);
+ }
+
+ return results;
+ }
+
+ private static FrugalList<SnapshotSpan> MapDownOneLevel(FrugalList<SnapshotSpan> inputSpans, Predicate<ITextSnapshot> match, ref ITextSnapshot chosenSnapshot, ref FrugalList<Span> targetSpans)
+ {
+ FrugalList<SnapshotSpan> downSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan inputSpan in inputSpans)
+ {
+ IProjectionBufferBase projBuffer = (IProjectionBufferBase)inputSpan.Snapshot.TextBuffer;
+ IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot;
+ if (projSnap.SourceSnapshots.Count > 0)
+ {
+ IList<SnapshotSpan> mappedSpans = projSnap.MapToSourceSnapshots(inputSpan);
+ for (int s = 0; s < mappedSpans.Count; ++s)
+ {
+ SnapshotSpan mappedSpan = mappedSpans[s];
+ ITextBuffer mappedBuffer = mappedSpan.Snapshot.TextBuffer;
+ if (mappedBuffer.CurrentSnapshot == chosenSnapshot)
+ {
+ targetSpans.Add(mappedSpan.Span);
+ }
+ else if (chosenSnapshot == null && match(mappedBuffer.CurrentSnapshot))
+ {
+ chosenSnapshot = mappedBuffer.CurrentSnapshot;
+ targetSpans.Add(mappedSpan.Span);
+ }
+ else
+ {
+ IProjectionBufferBase mappedProjBuffer = mappedBuffer as IProjectionBufferBase;
+ if (mappedProjBuffer != null)
+ {
+ downSpans.Add(mappedSpan);
+ }
+ }
+ }
+ }
+ }
+ return downSpans;
+ }
+
+ public NormalizedSnapshotSpanCollection MapUpToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, ITextBuffer targetBuffer)
+ {
+ if (!this.importingProjectionBufferMap.ContainsKey(targetBuffer))
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+ else
+ {
+ return CheckedMapUpToBuffer(span, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer));
+ }
+ }
+
+ public NormalizedSnapshotSpanCollection MapUpToFirstMatch(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ return CheckedMapUpToBuffer(span, trackingMode, match);
+ }
+
+ public NormalizedSnapshotSpanCollection CheckedMapUpToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match)
+ {
+ if (span.Snapshot == null)
+ {
+ throw new ArgumentNullException("span");
+ }
+ if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ ITextBuffer buffer = span.Snapshot.TextBuffer;
+ if (!this.importingProjectionBufferMap.ContainsKey(buffer))
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+ SnapshotSpan currentSpan = span.TranslateTo(buffer.CurrentSnapshot, trackingMode);
+ if (match(buffer.CurrentSnapshot))
+ {
+ return new NormalizedSnapshotSpanCollection(currentSpan);
+ }
+
+ ITextSnapshot chosenSnapshot = null;
+ FrugalList<Span> result = new FrugalList<Span>();
+ FrugalList<SnapshotSpan> spans = new FrugalList<SnapshotSpan>() { currentSpan };
+ do
+ {
+ spans = MapUpOneLevel(spans, ref chosenSnapshot, match, result);
+ } while (spans.Count > 0);
+
+ if (chosenSnapshot == null)
+ {
+ return NormalizedSnapshotSpanCollection.Empty;
+ }
+ else
+ {
+ return new NormalizedSnapshotSpanCollection(chosenSnapshot, result);
+ }
+ }
+
+ private FrugalList<SnapshotSpan> MapUpOneLevel(FrugalList<SnapshotSpan> spans, ref ITextSnapshot chosenSnapshot, Predicate<ITextSnapshot> match, FrugalList<Span> topSpans)
+ {
+ FrugalList<SnapshotSpan> upSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan span in spans)
+ {
+ FrugalList<IProjectionBufferBase> targetBuffers;
+ if (this.importingProjectionBufferMap.TryGetValue(span.Snapshot.TextBuffer, out targetBuffers))
+ {
+ if (targetBuffers != null) // make sure we don't fall off the top
+ {
+ foreach (IProjectionBufferBase projBuffer in targetBuffers)
+ {
+ IList<Span> projSpans = projBuffer.CurrentSnapshot.MapFromSourceSnapshot(span);
+ if (projBuffer.CurrentSnapshot == chosenSnapshot)
+ {
+ topSpans.AddRange(projSpans);
+ }
+ else if (chosenSnapshot == null && match(projBuffer.CurrentSnapshot))
+ {
+ chosenSnapshot = projBuffer.CurrentSnapshot;
+ topSpans.AddRange(projSpans);
+ }
+ else
+ {
+ foreach (Span projSpan in projSpans)
+ {
+ upSpans.Add(new SnapshotSpan(projBuffer.CurrentSnapshot, projSpan));
+ }
+ }
+ }
+ }
+ }
+ }
+ return upSpans;
+ }
+ #endregion
+
+ #region Event Handling
+ private class GraphEventRaiser : BaseBuffer.ITextEventRaiser
+ {
+ private BufferGraph graph;
+ private GraphBuffersChangedEventArgs args;
+
+ public GraphEventRaiser(BufferGraph graph, GraphBuffersChangedEventArgs args)
+ {
+ this.graph = graph;
+ this.args = args;
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ this.graph.RaiseGraphBuffersChanged(this.args);
+ }
+
+ public bool HasPostEvent
+ {
+ get { return false; }
+ }
+ }
+
+ public void RaiseGraphBuffersChanged(GraphBuffersChangedEventArgs args)
+ {
+ var listeners = GraphBuffersChanged;
+ if (listeners != null)
+ {
+ this.guardedOperations.RaiseEvent(this, listeners, args);
+ }
+ }
+
+ private void SourceBuffersChanged(object sender, ProjectionSourceBuffersChangedEventArgs e)
+ {
+ IProjectionBufferBase projBuffer = (IProjectionBufferBase)sender;
+ FrugalList<ITextBuffer> addedToGraphBuffers = new FrugalList<ITextBuffer>();
+ FrugalList<ITextBuffer> removedFromGraphBuffers = new FrugalList<ITextBuffer>();
+
+ foreach (ITextBuffer addedBuffer in e.AddedBuffers)
+ {
+ AddSourceBuffer(projBuffer, addedBuffer, addedToGraphBuffers);
+ }
+
+ foreach (ITextBuffer removedBuffer in e.RemovedBuffers)
+ {
+ RemoveSourceBuffer(projBuffer, removedBuffer, removedFromGraphBuffers);
+ }
+
+ if (addedToGraphBuffers.Count > 0 || removedFromGraphBuffers.Count > 0)
+ {
+ var listeners = GraphBuffersChanged;
+ if (listeners != null)
+ {
+ ((BaseBuffer)projBuffer).group.EnqueueEvents
+ (new GraphEventRaiser(this, new GraphBuffersChangedEventArgs(addedToGraphBuffers, removedFromGraphBuffers)), null);
+ }
+ }
+ }
+
+ private void AddSourceBuffer(IProjectionBufferBase projBuffer, ITextBuffer sourceBuffer, FrugalList<ITextBuffer> addedToGraphBuffers)
+ {
+ bool firstEncounter = false;
+ FrugalList<IProjectionBufferBase> importingProjectionBuffers;
+ if (!this.importingProjectionBufferMap.TryGetValue(sourceBuffer, out importingProjectionBuffers))
+ {
+ // sourceBuffer is new to this buffer graph
+ addedToGraphBuffers.Add(sourceBuffer);
+ firstEncounter = true;
+
+ importingProjectionBuffers = new FrugalList<IProjectionBufferBase>();
+ this.importingProjectionBufferMap.Add(sourceBuffer, importingProjectionBuffers);
+
+ this.eventHooks.Add(new WeakEventHookForBufferGraph(this, sourceBuffer));
+ }
+ importingProjectionBuffers.Add(projBuffer);
+
+ if (firstEncounter)
+ {
+ IProjectionBufferBase addedProjBufferBase = sourceBuffer as IProjectionBufferBase;
+ if (addedProjBufferBase != null)
+ {
+ foreach (ITextBuffer furtherSourceBuffer in addedProjBufferBase.SourceBuffers)
+ {
+ AddSourceBuffer(addedProjBufferBase, furtherSourceBuffer, addedToGraphBuffers);
+ }
+ }
+ }
+ }
+
+ private void RemoveSourceBuffer(IProjectionBufferBase projBuffer, ITextBuffer sourceBuffer, FrugalList<ITextBuffer> removedFromGraphBuffers)
+ {
+ IList<IProjectionBufferBase> importingProjectionBuffers = this.importingProjectionBufferMap[sourceBuffer];
+ importingProjectionBuffers.Remove(projBuffer);
+ if (importingProjectionBuffers.Count == 0)
+ {
+ removedFromGraphBuffers.Add(sourceBuffer);
+ this.importingProjectionBufferMap.Remove(sourceBuffer);
+
+ for (int i = 0; (i < this.eventHooks.Count); ++i)
+ {
+ if (this.eventHooks[i].SourceBuffer == sourceBuffer)
+ {
+ this.eventHooks[i].UnsubscribeFromSourceBuffer();
+ this.eventHooks.RemoveAt(i);
+ break;
+ }
+ }
+
+ IProjectionBufferBase removedProjBufferBase = sourceBuffer as IProjectionBufferBase;
+ if (removedProjBufferBase != null)
+ {
+ foreach (ITextBuffer furtherSourceBuffer in removedProjBufferBase.SourceBuffers)
+ {
+ RemoveSourceBuffer(removedProjBufferBase, furtherSourceBuffer, removedFromGraphBuffers);
+ }
+ }
+ }
+ }
+
+ protected void ContentTypeChanged(object sender, ContentTypeChangedEventArgs args)
+ {
+ // we do not subscribe to the immediate form of the sender's content type changed
+ // event, so we do not need to queue this event
+ var handler = GraphBufferContentTypeChanged;
+ if (handler != null)
+ {
+ handler(this, new GraphBufferContentTypeChangedEventArgs((ITextBuffer)sender, args.BeforeContentType, args.AfterContentType));
+ }
+ }
+
+ public event EventHandler<GraphBuffersChangedEventArgs> GraphBuffersChanged;
+ public event EventHandler<GraphBufferContentTypeChangedEventArgs> GraphBufferContentTypeChanged;
+
+ #endregion
+
+ /// <summary>
+ /// This an equivalent of the WeakEventHook, but for the buffer graph instead of being for a projection buffer.
+ /// </summary>
+ internal class WeakEventHookForBufferGraph
+ {
+ private readonly WeakReference<BufferGraph> _targetGraph;
+ private ITextBuffer _sourceBuffer;
+
+ public WeakEventHookForBufferGraph(BufferGraph targetGraph, ITextBuffer sourceBuffer)
+ {
+ _targetGraph = new WeakReference<BufferGraph>(targetGraph);
+ _sourceBuffer = sourceBuffer;
+
+ sourceBuffer.ContentTypeChanged += OnSourceBufferContentTypeChanged;
+ ProjectionBuffer projectionBuffer = sourceBuffer as ProjectionBuffer;
+ if (projectionBuffer != null)
+ {
+ projectionBuffer.SourceBuffersChangedImmediate += OnSourceBuffersChanged;
+ }
+ }
+
+ public ITextBuffer SourceBuffer { get { return _sourceBuffer; } }
+
+ public BufferGraph GetTargetGraph() // Not a property since it has side-effects
+ {
+ BufferGraph targetGraph;
+ if (_targetGraph.TryGetTarget(out targetGraph))
+ {
+ return targetGraph;
+ }
+
+ // The target buffer that was listening to events on the source buffer has died (no one was using it).
+ // Dead buffers tell no tales so they get to stop listening to tales as well. Unsubscribe from the
+ // events it hooked on the source buffer.
+ this.UnsubscribeFromSourceBuffer();
+ return null;
+ }
+
+ private void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e)
+ {
+ BufferGraph targetGraph = this.GetTargetGraph();
+ if (targetGraph != null)
+ {
+ targetGraph.ContentTypeChanged(sender, e);
+ }
+ }
+
+ private void OnSourceBuffersChanged(object sender, ProjectionSourceBuffersChangedEventArgs e)
+ {
+ BufferGraph targetGraph = this.GetTargetGraph();
+ if (targetGraph != null)
+ {
+ targetGraph.SourceBuffersChanged(sender, e);
+ }
+ }
+
+ public void UnsubscribeFromSourceBuffer()
+ {
+ if (_sourceBuffer != null)
+ {
+ _sourceBuffer.ContentTypeChanged -= OnSourceBufferContentTypeChanged;
+ ProjectionBuffer projectionBuffer = _sourceBuffer as ProjectionBuffer;
+ if (projectionBuffer != null)
+ {
+ projectionBuffer.SourceBuffersChangedImmediate -= OnSourceBuffersChanged;
+ }
+
+ _sourceBuffer = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs
new file mode 100644
index 0000000..7c47c06
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs
@@ -0,0 +1,29 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ [Export(typeof(IBufferGraphFactoryService))]
+ internal sealed class BufferGraphFactoryService : IBufferGraphFactoryService
+ {
+ [Import]
+ internal GuardedOperations GuardedOperations = null;
+
+ public IBufferGraph CreateBufferGraph(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ return textBuffer.Properties.GetOrCreateSingletonProperty<BufferGraph>(() => (new BufferGraph(textBuffer, GuardedOperations)));
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs
new file mode 100644
index 0000000..f47748e
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs
@@ -0,0 +1,489 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Utilities;
+
+ internal sealed class ElisionBuffer : BaseProjectionBuffer, IElisionBuffer
+ {
+ #region ElisionEdit class
+ private class ElisionEdit : Edit, ISubordinateTextEdit
+ {
+ private ElisionBuffer elisionBuffer;
+ private bool subordinate;
+
+ public ElisionEdit(ElisionBuffer elisionBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag)
+ : base(elisionBuffer, originSnapshot, options, reiteratedVersionNumber, editTag)
+ {
+ this.elisionBuffer = elisionBuffer;
+ this.subordinate = true;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.elisionBuffer; }
+ }
+
+ // this is the master edit path -- initiated from outside
+ protected override ITextSnapshot PerformApply()
+ {
+ CheckActive();
+ this.applied = true;
+ this.subordinate = false;
+
+ ITextSnapshot result = this.baseBuffer.currentSnapshot;
+
+ if (this.changes.Count > 0)
+ {
+ this.elisionBuffer.group.PerformMasterEdit(this.elisionBuffer, this, this.options, this.editTag);
+
+ if (!this.Canceled)
+ {
+ result = this.baseBuffer.currentSnapshot;
+ }
+ }
+ else
+ {
+ // vacuous edit
+ this.baseBuffer.editInProgress = false;
+ }
+
+ return result;
+ }
+
+ public void PreApply()
+ {
+ if (this.changes.Count > 0)
+ {
+ this.elisionBuffer.ComputeSourceEdits(this.changes);
+ }
+ }
+
+ public void FinalApply()
+ {
+ if (this.changes.Count > 0 || this.elisionBuffer.pendingContentChangedEventArgs.Count > 0)
+ {
+ this.elisionBuffer.group.CancelIndependentEdit(this.elisionBuffer); // just in case
+ TextContentChangedEventRaiser raiser = this.elisionBuffer.IncorporateChanges();
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ raiser.RaiseEvent(this.baseBuffer, true);
+ }
+
+ this.elisionBuffer.editInProgress = false;
+ this.elisionBuffer.editApplicationInProgress = false;
+ if (this.subordinate)
+ {
+ this.elisionBuffer.group.FinishEdit();
+ }
+ }
+
+ public override void CancelApplication()
+ {
+ if (!this.Canceled)
+ {
+ base.CancelApplication();
+ this.elisionBuffer.editApplicationInProgress = false;
+ this.elisionBuffer.pendingContentChangedEventArgs.Clear();
+ }
+ }
+ }
+ #endregion
+
+ #region Private members, Construction, and Disposal
+ private ElisionBufferOptions elisionOptions;
+ private ElisionMap content;
+ private ElisionSnapshot currentElisionSnapshot;
+ private readonly ITextBuffer sourceBuffer;
+ private ITextSnapshot sourceSnapshot;
+ private WeakEventHook eventHook;
+
+ public ElisionBuffer(IProjectionEditResolver resolver,
+ IContentType contentType,
+ ITextBuffer sourceBuffer,
+ NormalizedSpanCollection exposedSpans,
+ ElisionBufferOptions options,
+ ITextDifferencingService textDifferencingService,
+ GuardedOperations guardedOperations)
+ : base(resolver, contentType, textDifferencingService, guardedOperations)
+ {
+ Debug.Assert(sourceBuffer != null);
+ this.sourceBuffer = sourceBuffer;
+ this.sourceSnapshot = sourceBuffer.CurrentSnapshot;
+
+ BaseBuffer baseSourceBuffer = (BaseBuffer)sourceBuffer;
+
+ this.eventHook = new WeakEventHook(this, baseSourceBuffer);
+
+ this.group = baseSourceBuffer.group;
+ this.group.AddMember(this);
+
+ this.content = new ElisionMap(this.sourceSnapshot, exposedSpans);
+
+ StringRebuilder newBuilder = StringRebuilder.Empty;
+ for (int i = 0; (i < exposedSpans.Count); ++i)
+ newBuilder = newBuilder.Append(BufferFactoryService.StringRebuilderFromSnapshotAndSpan(this.sourceSnapshot, exposedSpans[i]));
+ this.builder = newBuilder;
+
+ this.elisionOptions = options;
+ this.currentVersion.SetLength(content.Length);
+ this.currentElisionSnapshot = new ElisionSnapshot(this, this.sourceSnapshot, base.currentVersion, this.builder, this.content, (options & ElisionBufferOptions.FillInMappingMode) != 0);
+ this.currentSnapshot = this.currentElisionSnapshot;
+ }
+ #endregion
+
+ #region Source Buffer
+ public override IList<ITextBuffer> SourceBuffers
+ {
+ get { return new FrugalList<ITextBuffer>() { this.sourceBuffer }; }
+ }
+
+ public ITextBuffer SourceBuffer
+ {
+ get { return this.sourceBuffer; }
+ }
+
+ public ElisionBufferOptions Options
+ {
+ get { return this.elisionOptions; }
+ }
+
+ #endregion
+
+ #region ElisionSourceSpansChangedEventRaiser Class
+ private class ElisionSourceSpansChangedEventRaiser : ITextEventRaiser
+ {
+ private readonly ElisionSourceSpansChangedEventArgs args;
+
+ public ElisionSourceSpansChangedEventRaiser(ElisionSourceSpansChangedEventArgs args)
+ {
+ this.args = args;
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ ElisionBuffer elBuffer = (ElisionBuffer)baseBuffer;
+ EventHandler<ElisionSourceSpansChangedEventArgs> spanHandlers = elBuffer.SourceSpansChanged;
+ if (spanHandlers != null)
+ {
+ spanHandlers(this, args);
+ }
+
+ // now raise the text content changed event
+ baseBuffer.RawRaiseEvent(args, immediate);
+ }
+
+ public bool HasPostEvent
+ {
+ get { return false; }
+ }
+ }
+ #endregion
+
+ #region Span Editing
+ private class SpanEdit : TextBufferBaseEdit
+ {
+ private readonly ElisionBuffer elBuffer;
+
+ public SpanEdit(ElisionBuffer elBuffer): base(elBuffer)
+ {
+ this.elBuffer = elBuffer;
+ }
+
+ public IProjectionSnapshot Apply(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand)
+ {
+ this.applied = true;
+ try
+ {
+ if (spansToElide == null)
+ {
+ spansToElide = NormalizedSpanCollection.Empty;
+ }
+ if (spansToExpand == null)
+ {
+ spansToExpand = NormalizedSpanCollection.Empty;
+ }
+ if (spansToElide.Count > 0 || spansToExpand.Count > 0)
+ {
+ if ((spansToElide.Count > 0) && (spansToElide[spansToElide.Count - 1].End > this.elBuffer.sourceSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("spansToElide");
+ }
+ if ((spansToExpand.Count > 0) && (spansToExpand[spansToExpand.Count - 1].End > this.elBuffer.sourceSnapshot.Length))
+ {
+ throw new ArgumentOutOfRangeException("spansToExpand");
+ }
+ ElisionSourceSpansChangedEventArgs args = this.elBuffer.ApplySpanChanges(spansToElide, spansToExpand);
+ if (args != null)
+ {
+ ElisionSourceSpansChangedEventRaiser raiser = new ElisionSourceSpansChangedEventRaiser(args);
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ raiser.RaiseEvent(this.baseBuffer, true);
+ }
+ this.baseBuffer.editInProgress = false;
+ }
+ else
+ {
+ this.baseBuffer.editInProgress = false;
+ }
+ }
+ finally
+ {
+ this.baseBuffer.group.FinishEdit();
+ }
+ return this.elBuffer.currentElisionSnapshot;
+ }
+ }
+
+ public IProjectionSnapshot ElideSpans(NormalizedSpanCollection spansToElide)
+ {
+ if (spansToElide == null)
+ {
+ throw new ArgumentNullException("spansToElide");
+ }
+ return ModifySpans(spansToElide, null);
+ }
+
+ public IProjectionSnapshot ExpandSpans(NormalizedSpanCollection spansToExpand)
+ {
+ if (spansToExpand == null)
+ {
+ throw new ArgumentNullException("spansToExpand");
+ }
+ return ModifySpans(null, spansToExpand);
+ }
+
+ public IProjectionSnapshot ModifySpans(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand)
+ {
+ using (SpanEdit spedit = new SpanEdit(this))
+ {
+ return spedit.Apply(spansToElide, spansToExpand);
+ }
+ }
+ #endregion
+
+ public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new ElisionEdit(this, this.currentElisionSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+
+ protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new ElisionEdit(this, this.currentElisionSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+
+ internal void ComputeSourceEdits(FrugalList<TextChange> changes)
+ {
+ ITextEdit xedit = this.group.GetEdit((BaseBuffer)this.sourceBuffer);
+ foreach (TextChange change in changes)
+ {
+ if (change.OldLength > 0)
+ {
+ IList<SnapshotSpan> sourceDeletionSpans = this.currentElisionSnapshot.MapToSourceSnapshots(new Span(change.OldPosition, change.OldLength));
+ foreach (SnapshotSpan sourceDeletionSpan in sourceDeletionSpans)
+ {
+ xedit.Delete(sourceDeletionSpan);
+ }
+ }
+ if (change.NewLength > 0)
+ {
+ // change includes an insertion
+ ReadOnlyCollection<SnapshotPoint> sourceInsertionPoints = this.currentElisionSnapshot.MapInsertionPointToSourceSnapshots(change.OldPosition, null);
+
+ if (sourceInsertionPoints.Count == 1)
+ {
+ // the insertion point is unambiguous
+ xedit.Insert(sourceInsertionPoints[0].Position, change.NewText);
+ }
+ else
+ {
+ // the insertion is at the boundary of source spans
+ int[] insertionSizes = new int[sourceInsertionPoints.Count];
+
+ if (this.resolver != null)
+ {
+ this.resolver.FillInInsertionSizes(new SnapshotPoint(this.currentElisionSnapshot, change.OldPosition),
+ sourceInsertionPoints, change.NewText, insertionSizes);
+ }
+
+ // if resolver was not provided, we just use zeros for the insertion sizes, which will push the entire insertion
+ // into the last slot.
+
+ int pos = 0;
+ for (int i = 0; i < insertionSizes.Length; ++i)
+ {
+ // contend with any old garbage that the client passed back.
+ int size = (i == insertionSizes.Length - 1)
+ ? change.NewLength - pos
+ : Math.Min(insertionSizes[i], change.NewLength - pos);
+ if (size > 0)
+ {
+ xedit.Insert(sourceInsertionPoints[i].Position, TextChange.ChangeNewSubstring(change, pos, size));
+ pos += size;
+ if (pos == change.NewLength)
+ {
+ break; // inserted text is used up, whether we've visited all of the insertionSizes or not
+ }
+ }
+ }
+ }
+ }
+ }
+ this.editApplicationInProgress = true;
+ }
+
+ public override BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag)
+ {
+ TextContentChangedEventRaiser raiser = IncorporateChanges();
+ raiser.RaiseEvent(this, true);
+ return raiser;
+ }
+
+ #region ChangeApplication
+
+ private ElisionSourceSpansChangedEventArgs ApplySpanChanges(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand)
+ {
+ ElisionSnapshot beforeSnapshot = this.currentElisionSnapshot;
+ FrugalList<TextChange> textChanges;
+ ElisionMap newContent = this.content.EditSpans(this.sourceSnapshot, spansToElide, spansToExpand, out textChanges);
+ if (newContent != this.content)
+ {
+ this.content = newContent;
+ INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(textChanges);
+ SetCurrentVersionAndSnapshot(normalizedChanges);
+ return new ElisionSourceSpansChangedEventArgs(beforeSnapshot, this.currentElisionSnapshot,
+ spansToElide, spansToExpand, null);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ #endregion
+
+ #region Snapshots
+
+ public override IProjectionSnapshot CurrentSnapshot
+ {
+ get { return this.currentElisionSnapshot; }
+ }
+
+ protected override BaseProjectionSnapshot CurrentBaseSnapshot
+ {
+ get { return this.currentElisionSnapshot; }
+ }
+
+ IElisionSnapshot IElisionBuffer.CurrentSnapshot
+ {
+ get { return this.currentElisionSnapshot; }
+ }
+
+ protected override StringRebuilder GetDoppelgangerBuilder()
+ {
+ // If there are no elisions, we can simply reference the source snapshot.
+ var sourceSnapshot = this.sourceSnapshot;
+ if ((this.currentVersion.Length == sourceSnapshot.Length) && (sourceSnapshot is BaseSnapshot))
+ {
+ return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, new Span(0, sourceSnapshot.Length));
+ }
+
+ return null;
+ }
+
+ protected override BaseSnapshot TakeSnapshot()
+ {
+ this.currentElisionSnapshot =
+ new ElisionSnapshot(this, this.sourceSnapshot, this.currentVersion, this.builder, this.content,
+ (this.elisionOptions & ElisionBufferOptions.FillInMappingMode) != 0);
+ return this.currentElisionSnapshot;
+ }
+ #endregion
+
+ private TextContentChangedEventRaiser IncorporateChanges()
+ {
+ Debug.Assert(this.sourceSnapshot == this.pendingContentChangedEventArgs[0].Before);
+ FrugalList<TextChange> projectedChanges = new FrugalList<TextChange>();
+
+ var args0 = this.pendingContentChangedEventArgs[0];
+ INormalizedTextChangeCollection sourceChanges;
+
+ // Separate the easy and common case:
+ if (this.pendingContentChangedEventArgs.Count == 1)
+ {
+ sourceChanges = args0.Changes;
+ this.sourceSnapshot = args0.After;
+ }
+ else
+ {
+ // there is more than one snapshot of the source buffer to deal with. Since the changes may be
+ // interleaved by position, we need to get a normalized list in sequence. First we denormalize the
+ // changes so they are all relative to the same single starting snapshot, then we normalize them again into
+ // a single list.
+
+ // This relies crucially on the fact that we know something about the multiple snapshots: they were
+ // induced by projection span adjustments, and the changes across them are independent. That is to say,
+ // it is not the case that text inserted in one snapshot is deleted in a later snapshot in the series.
+
+ DumpPendingContentChangedEventArgs();
+ List<TextChange> denormalizedChanges = new List<TextChange>() { new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None) };
+ for (int a = 0; a < this.pendingContentChangedEventArgs.Count; ++a)
+ {
+ NormalizedTextChangeCollection.Denormalize(this.pendingContentChangedEventArgs[a].Changes, denormalizedChanges);
+ }
+ DumpPendingChanges(new List<Tuple<ITextBuffer, List<TextChange>>>() { new Tuple<ITextBuffer, List<TextChange>>(this.sourceBuffer, denormalizedChanges) } );
+ FrugalList<TextChange> slicedChanges = new FrugalList<TextChange>();
+
+ // remove the sentinel
+ for (int d = 0; d < denormalizedChanges.Count - 1; ++d)
+ {
+ slicedChanges.Add(denormalizedChanges[d]);
+ }
+ sourceChanges = NormalizedTextChangeCollection.Create(slicedChanges);
+ this.sourceSnapshot = this.pendingContentChangedEventArgs[this.pendingContentChangedEventArgs.Count - 1].After;
+ }
+
+ if (sourceChanges.Count > 0)
+ {
+ this.content = this.content.IncorporateChanges(sourceChanges, projectedChanges, args0.Before, this.sourceSnapshot, this.currentElisionSnapshot);
+ }
+
+ this.pendingContentChangedEventArgs.Clear();
+ ElisionSnapshot beforeSnapshot = this.currentElisionSnapshot;
+ SetCurrentVersionAndSnapshot(NormalizedTextChangeCollection.Create(projectedChanges));
+ this.editApplicationInProgress = false;
+ return new TextContentChangedEventRaiser(beforeSnapshot, this.currentElisionSnapshot, args0.Options, args0.EditTag);
+ }
+
+ #region Event Handling
+ internal override void OnSourceTextChanged(object sender, TextContentChangedEventArgs e)
+ {
+ this.pendingContentChangedEventArgs.Add(e);
+
+ if (!this.editApplicationInProgress)
+ {
+ // We had better be a member of the same group as the buffer that we just heard from
+ Debug.Assert(this.group.MembersContains(e.After.TextBuffer));
+ // Let the buffer group decide when to issue events (this allows us to coalesce multiple snapshots
+ // from the source buffer (this can happen if the source buffer is a projection buffer) into a single snapshot here.
+ this.group.ScheduleIndependentEdit(this);
+ }
+ }
+ #endregion
+
+ #region Public Events
+ public event EventHandler<ElisionSourceSpansChangedEventArgs> SourceSpansChanged;
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ElisionMap.cs b/src/Text/Impl/TextModel/Projection/ElisionMap.cs
new file mode 100644
index 0000000..465077f
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ElisionMap.cs
@@ -0,0 +1,330 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ using Strings = Microsoft.VisualStudio.Text.Implementation.Strings;
+
+ internal class ElisionMap
+ {
+ private readonly ElisionMapNode root;
+ private readonly int spanCount;
+
+ #region Construction
+ /// <summary>
+ /// Construct an elision map given the size of the source buffer and the set of exposed spans.
+ /// </summary>
+ public ElisionMap(ITextSnapshot sourceSnapshot, NormalizedSpanCollection exposedSpans)
+ {
+ this.spanCount = exposedSpans.Count;
+ if (exposedSpans.Count == 0)
+ {
+ // everything is hidden: one leaf node
+ this.root = new ElisionMapNode(0, sourceSnapshot.Length, 0, sourceSnapshot.LineCount - 1, true);
+ }
+ else
+ {
+ // build a nicely balanced tree
+ // calculate all the line numbers we'll need in advance
+ int[] lineNumbers = new int[exposedSpans.Count * 2 + 1];
+ for (int es = 0; es < exposedSpans.Count; ++es)
+ {
+ lineNumbers[es * 2] = sourceSnapshot.GetLineNumberFromPosition(exposedSpans[es].Start);
+ lineNumbers[es * 2 + 1] = sourceSnapshot.GetLineNumberFromPosition(exposedSpans[es].End);
+ }
+ lineNumbers[exposedSpans.Count * 2] = sourceSnapshot.LineCount - 1;
+ this.root = Build(new SnapshotSpan(sourceSnapshot, 0, sourceSnapshot.Length), exposedSpans, lineNumbers, new Span(0, exposedSpans.Count));
+ }
+ if (BufferGroup.Tracing)
+ {
+ root.Dump(0);
+ }
+ }
+
+ private ElisionMap(ElisionMapNode root, int spanCount)
+ {
+ this.root = root;
+ this.spanCount = spanCount;
+ }
+
+ /// <summary>
+ /// Recursively build span tree.
+ /// </summary>
+ /// <param name="sourceSpan">SnapshotSpan over the source segment covered by this subtree, including both exposed and hidden text.</param>
+ /// <param name="exposedSpans">Set of exposed spans for the entire buffer.</param>
+ /// <param name="lineNumbers">Precomputed line numbers at all seams.</param>
+ /// <param name="slice">The slice of exposed spans in this subtree.</param>
+ /// <returns></returns>
+ private ElisionMapNode Build(SnapshotSpan sourceSpan, NormalizedSpanCollection exposedSpans, int[] lineNumbers, Span slice)
+ {
+ int mid = slice.Start + (slice.Length / 2);
+ Span midExposedSpan = exposedSpans[mid];
+
+ Span leftSlice = Span.FromBounds(slice.Start, mid);
+ ElisionMapNode left;
+ Span leftSpan;
+ if (leftSlice.Length > 0)
+ {
+ leftSpan = Span.FromBounds(sourceSpan.Start, midExposedSpan.Start);
+ left = Build(new SnapshotSpan(sourceSpan.Snapshot, leftSpan), exposedSpans, lineNumbers, leftSlice);
+ Debug.Assert(left.TotalSourceSize == leftSpan.Length);
+ }
+ else if (slice.Start == 0 && midExposedSpan.Start != 0)
+ {
+ Debug.Assert(sourceSpan.Start == 0);
+ leftSpan = Span.FromBounds(0, midExposedSpan.Start);
+ // the beginning of the buffer is elided. Do the special case of the first
+ // node in the tree having an exposed size of zero.
+ // TODO: figure this out in advance so we don't screw up the balance of the tree
+ left = new ElisionMapNode(0, leftSpan.Length, 0,
+ TextUtilities.ScanForLineCount(sourceSpan.Snapshot.GetText(leftSpan)),
+ true);
+ }
+ else
+ {
+ leftSpan = new Span(midExposedSpan.Start, 0);
+ left = null;
+ }
+
+ Span rightSlice = Span.FromBounds(mid + 1, slice.End);
+ ElisionMapNode right;
+ Span rightSpan;
+ if (rightSlice.Length > 0)
+ {
+ rightSpan = Span.FromBounds(exposedSpans[mid + 1].Start, sourceSpan.End);
+ right = Build(new SnapshotSpan(sourceSpan.Snapshot, rightSpan), exposedSpans, lineNumbers, rightSlice);
+ Debug.Assert(right.TotalSourceSize == rightSpan.Length);
+ }
+ else
+ {
+ rightSpan = new Span(sourceSpan.End, 0);
+ right = null;
+ }
+
+ Span midHiddenSpan = Span.FromBounds(midExposedSpan.End, rightSpan.Start);
+ ITextSnapshot sourceSnapshot = sourceSpan.Snapshot;
+
+ int startLineNumber = lineNumbers[2 * mid];
+ int endExposedLineNumber = lineNumbers[2 * mid + 1];
+ int endSourceLineNumber = lineNumbers[2 * mid + 2];
+
+ int exposedLineBreakCount = endExposedLineNumber - startLineNumber;
+ int hiddenLineBreakCount = endSourceLineNumber - endExposedLineNumber;
+
+ return new ElisionMapNode(midExposedSpan.Length,
+ sourceSpan.Length - (leftSpan.Length + rightSpan.Length),
+ exposedLineBreakCount,
+ exposedLineBreakCount + hiddenLineBreakCount,
+ left,
+ right,
+ false);
+ }
+ #endregion
+
+ public int Length
+ {
+ get { return this.root.TotalExposedSize; }
+ }
+
+ public int LineCount
+ {
+ get { return this.root.TotalExposedLineBreakCount + 1; }
+ }
+
+ public int SpanCount
+ {
+ get { return spanCount; }
+ }
+
+ public ElisionMap EditSpans(ITextSnapshot sourceSnapshot,
+ NormalizedSpanCollection spansToElide,
+ NormalizedSpanCollection spansToExpand,
+ out FrugalList<TextChange> textChanges)
+ {
+ textChanges = new FrugalList<TextChange>();
+ NormalizedSpanCollection beforeSourceSpans = new NormalizedSnapshotSpanCollection(GetSourceSpans(sourceSnapshot, 0, this.spanCount));
+
+ NormalizedSpanCollection afterElisionSourceSpans = NormalizedSpanCollection.Difference(beforeSourceSpans, spansToElide);
+ NormalizedSpanCollection elisionChangeSpans = NormalizedSpanCollection.Difference(beforeSourceSpans, afterElisionSourceSpans);
+ foreach (Span s in elisionChangeSpans)
+ {
+ textChanges.Add(TextChange.Create(this.root.MapFromSourceSnapshotToNearest(s.Start, 0),
+ BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, s),
+ StringRebuilder.Empty,
+ sourceSnapshot));
+ }
+
+ NormalizedSpanCollection afterExpansionSourceSpans = NormalizedSpanCollection.Union(afterElisionSourceSpans, spansToExpand);
+ NormalizedSpanCollection expansionChangeSpans = NormalizedSpanCollection.Difference(afterExpansionSourceSpans, afterElisionSourceSpans);
+ foreach (Span s in expansionChangeSpans)
+ {
+ textChanges.Add(TextChange.Create(this.root.MapFromSourceSnapshotToNearest(s.Start, 0),
+ StringRebuilder.Empty,
+ BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, s),
+ sourceSnapshot));
+ }
+
+ return textChanges.Count > 0 ? new ElisionMap(sourceSnapshot, afterExpansionSourceSpans) : this;
+ }
+
+ public IList<SnapshotSpan> GetSourceSpans(ITextSnapshot sourceSnapshot, int startSpanIndex, int count)
+ {
+ FrugalList<SnapshotSpan> result = new FrugalList<SnapshotSpan>();
+ int rank = -1;
+ int sourcePrefixSize = 0;
+ this.root.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, startSpanIndex + count, result);
+ return result;
+ }
+
+ public SnapshotPoint MapToSourceSnapshot(ITextSnapshot sourceSnapshot, int position, PositionAffinity affinity)
+ {
+ return this.root.MapToSourceSnapshot(sourceSnapshot, position, 0, affinity);
+ }
+
+ public SnapshotPoint? MapFromSourceSnapshot(ITextSnapshot snapshot, int position)
+ {
+ // affinity is superfluous for elision buffers
+ return this.root.MapFromSourceSnapshot(snapshot, position, 0);
+ }
+
+ public SnapshotPoint MapFromSourceSnapshotToNearest(ITextSnapshot snapshot, int position)
+ {
+ return new SnapshotPoint(snapshot, this.root.MapFromSourceSnapshotToNearest(position, 0));
+ }
+
+ public void MapToSourceSnapshots(IElisionSnapshot elisionSnapshot, Span span, FrugalList<SnapshotSpan> result)
+ {
+ if (span.Length == 0)
+ {
+ span = MapNullSpansToSourceSnapshots(elisionSnapshot, span, result);
+ }
+ else
+ {
+ this.root.MapToSourceSnapshots(elisionSnapshot.SourceSnapshot, span, 0, result);
+ }
+
+#if DEBUG
+ int length = 0;
+ foreach (SnapshotSpan ss in result)
+ {
+ length += ss.Length;
+ }
+ Debug.Assert(length == span.Length);
+#endif
+ }
+
+ private Span MapNullSpansToSourceSnapshots(IElisionSnapshot elisionSnapshot, Span span, FrugalList<SnapshotSpan> result)
+ {
+ // TODO: this is identical to projection snapshot; can it be shared?
+ FrugalList<SnapshotPoint> points = MapInsertionPointToSourceSnapshots(elisionSnapshot, span.Start);
+ for (int p = 0; p < points.Count; ++p)
+ {
+ SnapshotPoint point = points[p];
+ SnapshotSpan mappedSpan = new SnapshotSpan(point.Snapshot, point.Position, 0);
+ if (result.Count == 0 || mappedSpan != result[result.Count - 1])
+ {
+ result.Add(mappedSpan);
+ }
+ }
+ return span;
+ }
+
+ public void MapToSourceSnapshotsInFillInMode(ITextSnapshot sourceSnapshot, Span span, FrugalList<SnapshotSpan> result)
+ {
+ SnapshotPoint? start;
+ SnapshotPoint? end;
+ if (span.Length == 0)
+ {
+ start = span.Start == 0 ? new SnapshotPoint(sourceSnapshot, 0) : this.root.MapToSourceSnapshot(sourceSnapshot, span.Start, 0, PositionAffinity.Predecessor);
+ end = span.End == this.Length ? new SnapshotPoint(sourceSnapshot, sourceSnapshot.Length) : this.root.MapToSourceSnapshot(sourceSnapshot, span.End, 0, PositionAffinity.Successor);
+ }
+ else
+ {
+ start = this.root.MapToSourceSnapshot(sourceSnapshot, span.Start, 0, PositionAffinity.Successor);
+ end = this.root.MapToSourceSnapshot(sourceSnapshot, span.End, 0, PositionAffinity.Predecessor);
+ }
+ Debug.Assert(start.HasValue);
+ Debug.Assert(end.HasValue);
+ result.Add(new SnapshotSpan(sourceSnapshot, Span.FromBounds(start.Value, end.Value)));
+ }
+
+ public void MapFromSourceSnapshot(Span span, FrugalList<Span> result)
+ {
+ if (span.Length == 0)
+ {
+ this.root.MapNullSpanFromSourceSnapshot(span, 0, result);
+ }
+ else
+ {
+ this.root.MapFromSourceSnapshot(span, 0, result);
+ }
+ }
+
+ public FrugalList<SnapshotPoint> MapInsertionPointToSourceSnapshots(IElisionSnapshot elisionSnapshot, int exposedPosition)
+ {
+ FrugalList<SnapshotPoint> points = new FrugalList<SnapshotPoint>();
+ this.root.MapInsertionPointToSourceSnapshots(elisionSnapshot, exposedPosition, 0, points);
+ return points;
+ }
+
+ public int GetLineNumberFromPosition(int position, ITextSnapshot sourceSnapshot)
+ {
+ return this.root.GetLineNumberFromPosition(sourceSnapshot, position, 0, 0);
+ }
+
+ public ElisionMap IncorporateChanges(INormalizedTextChangeCollection sourceChanges,
+ FrugalList<TextChange> projectedChanges,
+ ITextSnapshot beforeSourceSnapshot,
+ ITextSnapshot sourceSnapshot,
+ ITextSnapshot beforeElisionSnapshot)
+ {
+ ElisionMapNode newRoot = this.root;
+ int accumulatedProjectedDelta = 0;
+ foreach (ITextChange sourceChange in sourceChanges)
+ {
+ int accumulatedDelete = 0;
+ int incrementalAccumulatedProjectedDelta = 0;
+ StringRebuilder newText = TextChange.NewStringRebuilder(sourceChange);
+
+ newRoot = newRoot.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot,
+ afterSourceSnapshot : sourceSnapshot,
+ beforeElisionSnapshot : beforeElisionSnapshot,
+ sourceInsertionPosition : sourceChange.NewLength > 0 ? sourceChange.NewPosition : (int?)null,
+ newText : newText,
+ sourceDeletionSpan : new Span(sourceChange.NewPosition, sourceChange.OldLength),
+ absoluteSourceOldPosition : sourceChange.OldPosition,
+ absoluteSourceNewPosition : sourceChange.NewPosition,
+ projectedPrefixSize : 0,
+ projectedChanges : projectedChanges,
+ incomingAccumulatedDelta : accumulatedProjectedDelta,
+ outgoingAccumulatedDelta : ref incrementalAccumulatedProjectedDelta,
+ accumulatedDelete : ref accumulatedDelete);
+ accumulatedProjectedDelta += incrementalAccumulatedProjectedDelta;
+ }
+ if (newRoot.TotalSourceSize != sourceSnapshot.Length)
+ {
+ Debug.Fail(String.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "Change incorporation length inconsistency. Elision:{0} Source:{1}", newRoot.TotalSourceSize, sourceSnapshot.Length));
+ throw new InvalidOperationException(Strings.InvalidLengthCalculation);
+ }
+ if (newRoot.TotalSourceLineBreakCount + 1 != sourceSnapshot.LineCount)
+ {
+ Debug.Fail(String.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "Change incorporation line count inconsistency. Elision:{0} Source:{1}", newRoot.TotalSourceLineBreakCount + 1, sourceSnapshot.LineCount));
+ throw new InvalidOperationException(Strings.InvalidLineCountCalculation);
+ }
+ return new ElisionMap(newRoot, this.spanCount);
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs b/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs
new file mode 100644
index 0000000..8a93c6d
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs
@@ -0,0 +1,1184 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal class ElisionMapNode
+ {
+ #region State
+ private readonly bool leftmostElision; // it would be better to avoid burning this space in every node
+
+ private readonly int exposedSize;
+ private readonly int sourceSize;
+ private readonly int exposedLineBreakCount;
+ private readonly int sourceLineBreakCount;
+
+ private readonly int totalExposedSize;
+ private readonly int totalSourceSize;
+ private readonly int totalExposedLineBreakCount;
+ private readonly int totalSourceLineBreakCount;
+
+ private readonly ElisionMapNode left;
+ private readonly ElisionMapNode right;
+ #endregion
+
+ #region Debugging Support
+ public override string ToString()
+ {
+ return String.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "Exp:{0} Src:{1} TExp:{2} Tsrc:{3}", this.exposedSize, this.sourceSize, this.totalExposedSize, this.totalSourceSize);
+ }
+ #endregion
+
+ #region Constructors
+ // leaf constructor
+ public ElisionMapNode(int exposedSize, int sourceSize, int exposedLineBreakCount, int sourceLineBreakCount, bool leftmostElision)
+ {
+ this.exposedSize = exposedSize;
+ this.sourceSize = sourceSize;
+ this.exposedLineBreakCount = exposedLineBreakCount;
+ this.sourceLineBreakCount = sourceLineBreakCount;
+
+ this.totalExposedSize = exposedSize;
+ this.totalSourceSize = sourceSize;
+ this.totalExposedLineBreakCount = exposedLineBreakCount;
+ this.totalSourceLineBreakCount = sourceLineBreakCount;
+
+ this.leftmostElision = leftmostElision;
+ }
+
+ // internal node constructor
+ public ElisionMapNode(int exposedSize, int sourceSize, int exposedLineBreakCount, int sourceLineBreakCount, ElisionMapNode left, ElisionMapNode right, bool leftmostElision)
+ {
+ this.exposedSize = exposedSize;
+ this.sourceSize = sourceSize;
+ this.exposedLineBreakCount = exposedLineBreakCount;
+ this.sourceLineBreakCount = sourceLineBreakCount;
+ this.left = left;
+ this.right = right;
+
+ this.totalExposedSize = LeftTotalExposedSize() + exposedSize + RightTotalExposedSize();
+ this.totalSourceSize = LeftTotalSourceSize() + sourceSize + RightTotalSourceSize();
+ this.totalExposedLineBreakCount = LeftTotalExposedLineBreakCount() + exposedLineBreakCount + RightTotalExposedLineBreakCount();
+ this.totalSourceLineBreakCount = LeftTotalSourceLineBreakCount() + sourceLineBreakCount + RightTotalSourceLineBreakCount();
+
+ this.leftmostElision = leftmostElision;
+ }
+ #endregion
+
+ #region Public Properties
+ public int TotalExposedSize
+ {
+ get { return this.totalExposedSize; }
+ }
+
+ public int TotalSourceSize
+ {
+ get { return this.totalSourceSize; }
+ }
+
+ public int TotalExposedLineBreakCount
+ {
+ get { return this.totalExposedLineBreakCount; }
+ }
+
+ public int TotalSourceLineBreakCount
+ {
+ get { return this.totalSourceLineBreakCount; }
+ }
+ #endregion
+
+ #region Helpers
+ private int LeftTotalSourceSize()
+ {
+ return this.left == null ? 0 : this.left.totalSourceSize;
+ }
+
+ private int LeftTotalExposedSize()
+ {
+ return this.left == null ? 0 : this.left.totalExposedSize;
+ }
+
+ private int LeftTotalHiddenSize()
+ {
+ return this.left == null ? 0 : this.left.totalSourceSize - this.left.totalExposedSize;
+ }
+
+ private int LeftTotalExposedLineBreakCount()
+ {
+ return this.left == null ? 0 : this.left.totalExposedLineBreakCount;
+ }
+
+ private int LeftTotalSourceLineBreakCount()
+ {
+ return this.left == null ? 0 : this.left.totalSourceLineBreakCount;
+ }
+
+ private int LeftTotalHiddenLineBreakCount()
+ {
+ return this.left == null ? 0 : this.left.totalSourceLineBreakCount - this.left.totalExposedLineBreakCount;
+ }
+
+ private int RightTotalSourceSize()
+ {
+ return this.right == null ? 0 : this.right.totalSourceSize;
+ }
+
+ private int RightTotalExposedSize()
+ {
+ return this.right == null ? 0 : this.right.totalExposedSize;
+ }
+
+ private int RightTotalExposedLineBreakCount()
+ {
+ return this.right == null ? 0 : this.right.totalExposedLineBreakCount;
+ }
+
+ private int RightTotalSourceLineBreakCount()
+ {
+ return this.right == null ? 0 : this.right.totalSourceLineBreakCount;
+ }
+
+ public void Dump(int level)
+ {
+ if (this.left != null)
+ {
+ this.left.Dump(level + 1);
+ }
+ Debug.Write(String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}{1},{2}({3},{4})\n", new string(' ', level * 3), this.exposedSize, this.sourceSize, this.totalExposedSize, this.totalSourceSize));
+ if (this.right != null)
+ {
+ this.right.Dump(level + 1);
+ }
+ }
+ #endregion
+
+ #region Source Spans
+ public void GetSourceSpans(ITextSnapshot sourceSnapshot, ref int rank, ref int sourcePrefixSize, int startSpanIndex, int endSpanIndex, IList<SnapshotSpan> result)
+ {
+ // We don't store explicit rank information in the nodes, so we walk the tree in inorder until we
+ // get to the starting position and continue until we have what we need.
+ if (this.left != null)
+ {
+ this.left.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, endSpanIndex, result);
+ }
+
+ if (!this.leftmostElision)
+ {
+ // don't count the possible special case node at the beginning;
+ rank++;
+ }
+ if (rank >= startSpanIndex && rank < endSpanIndex)
+ {
+ result.Add(new SnapshotSpan(sourceSnapshot, sourcePrefixSize, this.exposedSize));
+ }
+ sourcePrefixSize += this.sourceSize;
+
+ if (this.right != null)
+ {
+ this.right.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, endSpanIndex, result);
+ }
+ }
+ #endregion
+
+ #region Mapping
+ public SnapshotPoint MapToSourceSnapshot(ITextSnapshot sourceSnapshot, int exposedPosition, int sourcePrefixLength, PositionAffinity affinity)
+ {
+ Debug.Assert(exposedPosition >= 0);
+ Debug.Assert(exposedPosition <= this.totalExposedSize);
+
+ int leftTotalExposedSize = LeftTotalExposedSize();
+
+ if (((affinity == PositionAffinity.Predecessor) && (exposedPosition <= leftTotalExposedSize) && left != null && !left.leftmostElision) ||
+ ((affinity == PositionAffinity.Successor) && (exposedPosition < leftTotalExposedSize)))
+ {
+ return this.left.MapToSourceSnapshot(sourceSnapshot : sourceSnapshot,
+ exposedPosition : exposedPosition,
+ sourcePrefixLength : sourcePrefixLength,
+ affinity : affinity);
+ }
+
+ else if (((affinity == PositionAffinity.Predecessor) && (exposedPosition <= leftTotalExposedSize + this.exposedSize)) ||
+ ((affinity == PositionAffinity.Successor) && (exposedPosition < leftTotalExposedSize + this.exposedSize)))
+ {
+ return new SnapshotPoint(sourceSnapshot,
+ sourcePrefixLength + LeftTotalSourceSize() + (exposedPosition - leftTotalExposedSize));
+ }
+
+ else if (right == null)
+ {
+ // we are mapping the last exposedPosition in the buffer, that is, the one that is just past the end.
+ // ignore the affinity.
+ Debug.Assert(exposedPosition == leftTotalExposedSize + this.exposedSize);
+ return new SnapshotPoint(sourceSnapshot, sourcePrefixLength + LeftTotalSourceSize() + this.exposedSize);
+ }
+
+ else
+ {
+ return this.right.MapToSourceSnapshot(sourceSnapshot : sourceSnapshot,
+ exposedPosition : exposedPosition - (leftTotalExposedSize + this.exposedSize),
+ sourcePrefixLength : sourcePrefixLength + LeftTotalSourceSize() + this.sourceSize,
+ affinity : affinity);
+ }
+ }
+
+ public SnapshotPoint? MapFromSourceSnapshot(ITextSnapshot snapshot, int sourcePosition, int exposedPrefixLength)
+ {
+ Debug.Assert(sourcePosition <= this.totalSourceSize);
+ int leftTotalSourceSize = LeftTotalSourceSize();
+
+ if (sourcePosition < leftTotalSourceSize)
+ {
+ if (left == null)
+ {
+ return null;
+ }
+ else
+ {
+ return this.left.MapFromSourceSnapshot(snapshot, sourcePosition, exposedPrefixLength);
+ }
+ }
+ else if (sourcePosition < leftTotalSourceSize + this.sourceSize)
+ {
+ if ((sourcePosition <= leftTotalSourceSize + this.exposedSize) && (!this.leftmostElision))
+ {
+ return new SnapshotPoint(snapshot,
+ exposedPrefixLength + LeftTotalExposedSize() + (sourcePosition - leftTotalSourceSize));
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else if (right == null)
+ {
+ // We are mapping the last position in the source snapshot
+ if (this.exposedSize < this.sourceSize)
+ {
+ return null;
+ }
+ else
+ {
+ return new SnapshotPoint(snapshot, exposedPrefixLength + LeftTotalExposedSize() + this.exposedSize);
+ }
+ }
+ else
+ {
+ return this.right.MapFromSourceSnapshot(snapshot,
+ sourcePosition - (leftTotalSourceSize + this.sourceSize),
+ exposedPrefixLength + LeftTotalExposedSize() + this.exposedSize);
+ }
+ }
+
+ public int MapFromSourceSnapshotToNearest(int sourcePosition, int exposedPrefixLength)
+ {
+ Debug.Assert(sourcePosition <= this.totalSourceSize);
+
+ if (sourcePosition < LeftTotalSourceSize())
+ {
+ return this.left.MapFromSourceSnapshotToNearest(sourcePosition, exposedPrefixLength);
+ }
+ else
+ {
+ // shift coordinates to start with current node
+ exposedPrefixLength += LeftTotalExposedSize();
+ sourcePosition -= LeftTotalSourceSize();
+ if (sourcePosition < this.sourceSize)
+ {
+ if (sourcePosition < this.exposedSize)
+ {
+ // position is in the exposed segment
+ return exposedPrefixLength + sourcePosition;
+ }
+ else
+ {
+ // position is in the hidden segment
+ return exposedPrefixLength + this.exposedSize;
+ }
+ }
+ else if (right == null)
+ {
+ // We are mapping the last position in the source snapshot
+ return exposedPrefixLength + this.exposedSize;
+ }
+ else
+ {
+ // shift coordinates to right subtree
+ exposedPrefixLength += this.exposedSize;
+ sourcePosition -= this.sourceSize;
+ return this.right.MapFromSourceSnapshotToNearest(sourcePosition, exposedPrefixLength);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Map a span over the exposed text to spans on the source buffer.
+ /// </summary>
+ /// <param name="sourceSnapshot">Snapshot of the source buffer to which to map.</param>
+ /// <param name="mapSpan">The span to map, expressed in terms of exposed text.</param>
+ /// <param name="sourcePrefixSize">The amount of source text (hidden or exposed) that precedes the text in this subtree.</param>
+ /// <param name="result">List of spans to which to append the result, expressed in terms of source text.</param>
+ public void MapToSourceSnapshots(ITextSnapshot sourceSnapshot,
+ Span mapSpan,
+ int sourcePrefixSize,
+ FrugalList<SnapshotSpan> result)
+ {
+ // This method is written for maximum clarity
+
+ int leftTotalExposedSize = LeftTotalExposedSize();
+ Span leftSpan = new Span(0, leftTotalExposedSize);
+ Span midSpan = new Span(leftTotalExposedSize, this.exposedSize);
+ Span rightSpan = new Span(leftTotalExposedSize + this.exposedSize, RightTotalExposedSize());
+
+ // map the requested span to the left subtree, this node, and the right subtree, in terms of exposed text
+ Span? leftMapSpan = mapSpan.Overlap(leftSpan);
+ Span? midMapSpan = mapSpan.Overlap(midSpan);
+ Span? rightMapSpan = mapSpan.Overlap(rightSpan);
+
+ if (leftMapSpan != null)
+ {
+ this.left.MapToSourceSnapshots(sourceSnapshot,
+ leftMapSpan.Value,
+ sourcePrefixSize,
+ result);
+ }
+
+ if (midMapSpan != null)
+ {
+ // the source span for the current node has the same length as the midMapSpan, but the start is
+ // adjusted by the total amount of source text to the left of this node, which is the sourcePrefixSize plus
+ // the amount of text hidden in the left subtree.
+ Span sourceSpan = new Span(midMapSpan.Value.Start + sourcePrefixSize + LeftTotalHiddenSize(), midMapSpan.Value.Length);
+ result.Add(new SnapshotSpan(sourceSnapshot, sourceSpan));
+ }
+
+ if (rightMapSpan != null)
+ {
+ this.right.MapToSourceSnapshots(sourceSnapshot,
+ new Span(rightMapSpan.Value.Start - leftTotalExposedSize - this.exposedSize, rightMapSpan.Value.Length),
+ sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize,
+ result);
+ }
+ }
+
+ public void MapFromSourceSnapshot(Span mapSpan, int exposedPrefixSize, FrugalList<Span> result)
+ {
+ int leftTotalSourceSize = LeftTotalSourceSize();
+ Span leftSourceSpan = new Span(0, leftTotalSourceSize);
+ Span midExposedSpan = new Span(leftTotalSourceSize, this.exposedSize);
+ Span rightSourceSpan = new Span(leftTotalSourceSize + this.sourceSize, RightTotalSourceSize());
+
+ // map the requested span to the left subtree, this node, and the right subtree, in terms of source text
+ Span? leftMapSpan = mapSpan.Overlap(leftSourceSpan);
+ Span? midMapSpan = mapSpan.Overlap(midExposedSpan);
+ Span? rightMapSpan = mapSpan.Overlap(rightSourceSpan);
+
+ if (leftMapSpan != null)
+ {
+ this.left.MapFromSourceSnapshot(leftMapSpan.Value, exposedPrefixSize, result);
+ }
+
+ if (midMapSpan != null)
+ {
+ result.Add(new Span(midMapSpan.Value.Start + exposedPrefixSize - LeftTotalHiddenSize(), midMapSpan.Value.Length));
+ }
+
+ if (rightMapSpan != null)
+ {
+ this.right.MapFromSourceSnapshot(new Span(rightMapSpan.Value.Start - leftTotalSourceSize - this.sourceSize, rightMapSpan.Value.Length),
+ exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize,
+ result);
+ }
+ }
+
+ public void MapNullSpanFromSourceSnapshot(Span nullSourceSpan, int exposedPrefixSize, FrugalList<Span> result)
+ {
+ int leftTotalSourceSize = LeftTotalSourceSize();
+ Span midExposedSpan = new Span(leftTotalSourceSize, this.exposedSize);
+
+ if (this.left != null)
+ {
+ Span leftSourceSpan = new Span(0, leftTotalSourceSize);
+ if (leftSourceSpan.IntersectsWith(nullSourceSpan))
+ {
+ this.left.MapNullSpanFromSourceSnapshot(nullSourceSpan, exposedPrefixSize, result);
+ }
+ }
+
+ if (!this.leftmostElision && midExposedSpan.IntersectsWith(nullSourceSpan))
+ {
+ Span intersection = midExposedSpan.Intersection(nullSourceSpan).Value;
+ result.Add(new Span(intersection.Start + exposedPrefixSize - LeftTotalHiddenSize(), 0));
+ }
+
+ if (this.right != null)
+ {
+ Span rightSourceSpan = new Span(leftTotalSourceSize + this.sourceSize, RightTotalSourceSize());
+ if (rightSourceSpan.IntersectsWith(nullSourceSpan))
+ {
+ Span intersection = rightSourceSpan.Intersection(nullSourceSpan).Value;
+ this.right.MapNullSpanFromSourceSnapshot(new Span(intersection.Start - leftTotalSourceSize - this.sourceSize, 0),
+ exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize,
+ result);
+ }
+ }
+ }
+
+ public void MapInsertionPointToSourceSnapshots(IElisionSnapshot elisionSnapshot,
+ int exposedPosition,
+ int sourcePrefixLength,
+ FrugalList<SnapshotPoint> points)
+ {
+ Debug.Assert(exposedPosition >= 0);
+ Debug.Assert(exposedPosition <= this.totalExposedSize);
+
+ int leftTotalExposedSize = LeftTotalExposedSize();
+
+ if (this.left != null && exposedPosition <= leftTotalExposedSize)
+ {
+ this.left.MapInsertionPointToSourceSnapshots(elisionSnapshot,
+ exposedPosition,
+ sourcePrefixLength,
+ points);
+ }
+
+ bool ignoreThisNode = this.leftmostElision && elisionSnapshot.Length > 0;
+
+ if (!ignoreThisNode)
+ {
+ if (exposedPosition >= leftTotalExposedSize && exposedPosition <= leftTotalExposedSize + this.exposedSize)
+ {
+ points.Add(new SnapshotPoint(elisionSnapshot.SourceSnapshot, sourcePrefixLength + LeftTotalSourceSize() + (exposedPosition - leftTotalExposedSize)));
+ }
+ }
+
+ if (this.right != null && exposedPosition >= leftTotalExposedSize + this.exposedSize)
+ {
+ this.right.MapInsertionPointToSourceSnapshots(elisionSnapshot,
+ exposedPosition - (LeftTotalExposedSize() + this.exposedSize),
+ sourcePrefixLength + LeftTotalSourceSize() + this.sourceSize,
+ points);
+ }
+ }
+ #endregion
+
+ #region Lines
+
+ //private void TraceEnter(int level)
+ //{
+ // Debug.Write(new string(' ', level * 3));
+ // Debug.Write(this.exposedSize);
+ // Debug.Write(' ');
+ // Debug.WriteLine(this.sourceSize);
+ //}
+
+ //private static void TraceExit(int level, LineInfo info)
+ //{
+ // Debug.Write(new string(' ', level * 3));
+ // Debug.WriteLine(info.ToString());
+ //}
+
+ public ProjectionLineInfo GetLineFromPosition(ITextSnapshot sourceSnapshot,
+ int exposedPosition,
+ int sourcePrefixLineBreakCount,
+ int hiddenPrefixLineBreakCount,
+ int sourcePrefixSize,
+ int exposedPrefixSize,
+ int level)
+ {
+ //TraceEnter(level);
+ Span relativeExposedSpan = new Span(LeftTotalExposedSize(), this.exposedSize);
+ ProjectionLineCalculationState state = ProjectionLineCalculationState.Primary;
+ int relativeExposedPosition = exposedPosition;
+ ProjectionLineInfo pendingInfo = new ProjectionLineInfo();
+
+ do
+ {
+ if (relativeExposedPosition < relativeExposedSpan.Start)
+ {
+ #region relative ExposedPosition is in left subtree
+ Debug.Assert(state != ProjectionLineCalculationState.Append);
+
+ // recursively compute that part of the line that is in the left subtree
+ ProjectionLineInfo leftInfo = this.left.GetLineFromPosition
+ (sourceSnapshot : sourceSnapshot,
+ exposedPosition : relativeExposedPosition,
+ sourcePrefixLineBreakCount : sourcePrefixLineBreakCount,
+ hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount,
+ sourcePrefixSize : sourcePrefixSize,
+ exposedPrefixSize : exposedPrefixSize,
+ level : level + 1);
+
+ if (state == ProjectionLineCalculationState.Primary)
+ {
+ if (leftInfo.endComplete)
+ {
+ // leftInfo.startComplete may be false, but we aren't going to find anything
+ // further to the left at this level of the tree, so we are done here
+ // TraceExit(level, leftInfo);
+ return leftInfo;
+ }
+ else
+ {
+ // the end of the line extends into the current node. our new position
+ // of interest is the start of the exposed text in this node.
+ state = ProjectionLineCalculationState.Append;
+ pendingInfo = leftInfo;
+ relativeExposedPosition = relativeExposedSpan.Start;
+ continue;
+ }
+ }
+ else
+ {
+ // We've just been looking for the beginning of the line in the left subtree
+ Debug.Assert(state == ProjectionLineCalculationState.Prepend || state == ProjectionLineCalculationState.Bipend);
+ if (pendingInfo.lineNumber == leftInfo.lineNumber)
+ {
+ // the left node contained more of the line we are looking for
+ // we may or may not have found the start of the line, but there
+ // is no more to find at this level
+ pendingInfo.start = leftInfo.start;
+ pendingInfo.startComplete = leftInfo.startComplete;
+ }
+ else
+ {
+ // the left exposed source ended with a line break, so
+ // there is no change to the previously computed start
+ pendingInfo.startComplete = true;
+ }
+ if (state == ProjectionLineCalculationState.Bipend)
+ {
+ // now we need to look in the right subtree
+ state = ProjectionLineCalculationState.Append;
+ relativeExposedPosition = relativeExposedSpan.End;
+ continue;
+ }
+ else
+ {
+ // TraceExit(level, pendingInfo);
+ return pendingInfo;
+ }
+ }
+ #endregion
+ }
+ else if (relativeExposedPosition < relativeExposedSpan.End || this.right == null)
+ {
+ #region relative ExposedPosition is in current node
+ int absoluteSourcePosition = sourcePrefixSize + LeftTotalHiddenSize() + relativeExposedPosition;
+ ITextSnapshotLine sourceLine = sourceSnapshot.GetLineFromPosition(absoluteSourcePosition);
+ ProjectionLineCalculationState nextState = ProjectionLineCalculationState.Primary;
+ int provisionalLineNumber = sourceLine.LineNumber - (LeftTotalHiddenLineBreakCount() + hiddenPrefixLineBreakCount);
+
+ if (state == ProjectionLineCalculationState.Primary)
+ {
+ pendingInfo = new ProjectionLineInfo();
+
+ // the primary position we are searching for is in the current node.
+ // now we know the line number!
+ pendingInfo.lineNumber = provisionalLineNumber;
+ Debug.Assert(pendingInfo.lineNumber >= 0);
+ }
+
+ // compute the length of the portion of the source line that precedes the position we've been searching for
+ int sourceLeader = absoluteSourcePosition - sourceLine.Start;
+ // where would that map to in the current node?
+ int relativeExposedLineStart = relativeExposedPosition - sourceLeader;
+
+ if (state == ProjectionLineCalculationState.Prepend && provisionalLineNumber < pendingInfo.lineNumber)
+ {
+ // we were trying to pick up the beginning of a line that had been elided, but it was
+ // elided all the way to its beginning, so we are done.
+ pendingInfo.startComplete = true;
+ }
+ else if (state == ProjectionLineCalculationState.Primary || state == ProjectionLineCalculationState.Prepend)
+ {
+ // if we are lucky, the whole line will be contained in this node
+
+ if (relativeExposedLineStart > relativeExposedSpan.Start)
+ {
+ // we are sure that nothing to our left is of interest
+ pendingInfo.start = exposedPrefixSize + relativeExposedLineStart;
+ pendingInfo.startComplete = true;
+ }
+ else
+ {
+ // rats! part of the 'leader' is elided.
+ // start with the beginning of this segment
+ pendingInfo.start = exposedPrefixSize + relativeExposedSpan.Start;
+ pendingInfo.startComplete = false;
+ // and check further to the left if there is anything there
+ if (LeftTotalExposedSize() > 0)
+ {
+ nextState = ProjectionLineCalculationState.Prepend;
+ relativeExposedPosition = relativeExposedSpan.Start - 1;
+ }
+ }
+ }
+
+ if (state == ProjectionLineCalculationState.Primary || state == ProjectionLineCalculationState.Append)
+ {
+ int exposedLineEnd = relativeExposedLineStart + sourceLine.LengthIncludingLineBreak;
+ if (exposedLineEnd <= relativeExposedSpan.End)
+ {
+ // good!
+ pendingInfo.end = exposedLineEnd + exposedPrefixSize - sourceLine.LineBreakLength;
+ pendingInfo.endComplete = true;
+ pendingInfo.lineBreakLength = sourceLine.LineBreakLength;
+ }
+ else
+ {
+ pendingInfo.end = exposedPrefixSize + relativeExposedSpan.End;
+ pendingInfo.endComplete = false;
+ if (this.right == null)
+ {
+ if (nextState != ProjectionLineCalculationState.Prepend)
+ {
+ // we need to go further right but there is nothing more below us
+ // TraceExit(level, pendingInfo);
+ return pendingInfo;
+ }
+ }
+ else
+ {
+ if (nextState == ProjectionLineCalculationState.Prepend)
+ {
+ nextState = ProjectionLineCalculationState.Bipend;
+ }
+ else
+ {
+ nextState = ProjectionLineCalculationState.Append;
+ relativeExposedPosition = relativeExposedSpan.End;
+ }
+ }
+ }
+ }
+ if (nextState == ProjectionLineCalculationState.Primary)
+ {
+ // TraceExit(level, pendingInfo);
+ return pendingInfo;
+ }
+ state = nextState;
+ #endregion
+ }
+ else
+ {
+ #region relative ExposedPosition is in right subtree
+ Debug.Assert(state != ProjectionLineCalculationState.Bipend);
+
+ // recursively compute that part of the line that is in the right subtree
+ ProjectionLineInfo rightInfo = this.right.GetLineFromPosition
+ (sourceSnapshot : sourceSnapshot,
+ exposedPosition : relativeExposedPosition - (LeftTotalExposedSize() + this.exposedSize),
+ sourcePrefixLineBreakCount : sourcePrefixLineBreakCount + LeftTotalSourceLineBreakCount() + this.sourceLineBreakCount,
+ hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + (this.sourceLineBreakCount - this.exposedLineBreakCount),
+ sourcePrefixSize : sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize,
+ exposedPrefixSize : exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize,
+ level : level + 1);
+ if (state == ProjectionLineCalculationState.Primary)
+ {
+ if (rightInfo.startComplete)
+ {
+ // rightInfo.endComplete may be false, but we aren't going to find anything
+ // further to the right at this level of three, so we are done here
+ // TraceExit(level, rightInfo);
+ return rightInfo;
+ }
+ else
+ {
+ // the begnning of the line extends into the current node. our new position
+ // of interest is the end of the exposed text in this node.
+ state = ProjectionLineCalculationState.Prepend;
+ pendingInfo = rightInfo;
+ relativeExposedPosition = relativeExposedSpan.End - 1;
+ continue;
+ }
+ }
+ else
+ {
+ // We've just been looking for the end of the line in the right subtree
+ Debug.Assert(state == ProjectionLineCalculationState.Append);
+
+ // the first line we saw in the right subtree must have been the same line
+ // since there wasn't a line break at the end of the current node
+ Debug.Assert(pendingInfo.lineNumber == rightInfo.lineNumber);
+
+ pendingInfo.end = rightInfo.end;
+ pendingInfo.endComplete = rightInfo.endComplete;
+ pendingInfo.lineBreakLength = rightInfo.lineBreakLength;
+ // TraceExit(level, pendingInfo);
+ return pendingInfo;
+ }
+ #endregion
+ }
+ } while (relativeExposedPosition >= 0 && relativeExposedPosition <= this.totalExposedSize);
+
+ // TraceExit(level, pendingInfo);
+ return pendingInfo;
+ }
+
+ public ProjectionLineInfo GetLineFromLineNumber(ITextSnapshot sourceSnapshot,
+ int exposedLineNumber)
+ {
+ int pos = GetPositionFromLineNumber(sourceSnapshot, exposedLineNumber, 0, 0);
+ ProjectionLineInfo info = this.GetLineFromPosition(sourceSnapshot, pos, 0, 0, 0, 0, 0);
+ Debug.Assert(info.lineNumber == exposedLineNumber);
+ return info;
+ }
+
+ private int GetPositionFromLineNumber(ITextSnapshot sourceSnapshot,
+ int relativeExposedLineNumber,
+ int sourcePrefixLineBreakCount,
+ int sourcePrefixHiddenSize)
+
+ // return the absolute (exposed) position of the (first) line break character in the requested line
+ {
+ if (relativeExposedLineNumber < LeftTotalExposedLineBreakCount())
+ {
+ return this.left.GetPositionFromLineNumber
+ (sourceSnapshot : sourceSnapshot,
+ relativeExposedLineNumber : relativeExposedLineNumber,
+ sourcePrefixLineBreakCount : sourcePrefixLineBreakCount,
+ sourcePrefixHiddenSize : sourcePrefixHiddenSize);
+ }
+
+ else if (relativeExposedLineNumber < LeftTotalExposedLineBreakCount() + this.exposedLineBreakCount || this.right == null)
+ {
+ int absoluteSourceLineNumber = sourcePrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + relativeExposedLineNumber;
+ ITextSnapshotLine sourceLine = sourceSnapshot.GetLineFromLineNumber(absoluteSourceLineNumber);
+ return sourceLine.End - sourcePrefixHiddenSize - LeftTotalHiddenSize();
+ }
+
+ else
+ {
+ return this.right.GetPositionFromLineNumber
+ (sourceSnapshot : sourceSnapshot,
+ relativeExposedLineNumber : relativeExposedLineNumber - (LeftTotalExposedLineBreakCount() + this.exposedLineBreakCount),
+ sourcePrefixLineBreakCount : sourcePrefixLineBreakCount + LeftTotalSourceLineBreakCount() + this.sourceLineBreakCount,
+ sourcePrefixHiddenSize : sourcePrefixHiddenSize + LeftTotalHiddenSize() + (this.sourceSize - this.exposedSize));
+ }
+ }
+
+ public int GetLineNumberFromPosition(ITextSnapshot sourceSnapshot, int exposedPosition, int hiddenPrefixLineBreakCount, int sourcePrefixSize)
+ {
+ Span midSpan = new Span(LeftTotalExposedSize(), this.exposedSize);
+
+ if (exposedPosition < midSpan.Start)
+ {
+ return this.left.GetLineNumberFromPosition
+ (sourceSnapshot : sourceSnapshot,
+ exposedPosition : exposedPosition,
+ hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount,
+ sourcePrefixSize : sourcePrefixSize);
+ }
+ else if ((exposedPosition < midSpan.End) || this.right == null)
+ {
+ // if we took this branch because this.right is null, the position is the last position in the elision buffer
+ return sourceSnapshot.GetLineNumberFromPosition(exposedPosition + sourcePrefixSize + LeftTotalSourceSize() - LeftTotalExposedSize()) -
+ (LeftTotalHiddenLineBreakCount() + hiddenPrefixLineBreakCount);
+ }
+ else
+ {
+ return this.right.GetLineNumberFromPosition
+ (sourceSnapshot : sourceSnapshot,
+ exposedPosition : exposedPosition - midSpan.End,
+ hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + (this.sourceLineBreakCount - this.exposedLineBreakCount),
+ sourcePrefixSize : sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize);
+ }
+ }
+ #endregion
+
+ #region Change Incorporation
+ /// <summary>
+ /// Incorporate a text change in the source buffer into the elision map.
+ /// </summary>
+ /// <param name="beforeSourceSnapshot">Snapshot of the source buffer before the change occurred.</param>
+ /// <param name="beforeElisionSnapshot">Snapshot of the elision buffer before the change occurred.</param>
+ /// <param name="sourceInsertionPosition">If there is an insertion as part of the change, the position of that
+ /// insertion relative to the subtree rooted at this node; otherwise null.</param>
+ /// <param name="newText">New text to be inserted (or the null string).</param>
+ /// <param name="sourceDeletionSpan">If there is a deletion as part of the change, the span of that
+ /// deletion relative to the subtree rooted at this node; otherwise null.</param>
+ /// <param name="absoluteSourceOldPosition">The absolute position of the change in the <paramref name="beforeSourceSnapshot"/>.</param>
+ /// <param name="projectedPrefixSize">The size (in characters) of that portion of elision buffer that
+ /// precedes this node and is not in its left subtree.</param>
+ /// <param name="projectedChanges">List of changes projected into the elision buffer; constructed by
+ /// this function.</param>
+ /// <param name="incomingAccumulatedDelta">Accumulated delta from prior source changes in the current
+ /// multipart edit transaction.</param>
+ /// <param name="outgoingAccumulatedDelta">Increment to the accumulated delta resulting from the
+ /// current change.</param>
+ /// <param name="accumulatedDelete">Amount of text deleted so far for the current change.</param>
+ /// <returns>A new immutable ElisionMapNode.</returns>
+ public ElisionMapNode IncorporateChange(ITextSnapshot beforeSourceSnapshot,
+ ITextSnapshot afterSourceSnapshot,
+ ITextSnapshot beforeElisionSnapshot,
+ int? sourceInsertionPosition,
+ StringRebuilder newText,
+ Span? sourceDeletionSpan,
+ int absoluteSourceOldPosition,
+ int absoluteSourceNewPosition,
+ int projectedPrefixSize,
+ FrugalList<TextChange> projectedChanges,
+ int incomingAccumulatedDelta,
+ ref int outgoingAccumulatedDelta,
+ ref int accumulatedDelete)
+ {
+ // All positions and spans in this method are relative to the current subtree except those with 'absolute' in the name
+ // All positions and spans in coordinate space of source buffer have 'source' in the name
+ // All positions and spans in coordinate space of elision buffer have 'projected' in the name
+
+ Debug.Assert(sourceDeletionSpan == null || sourceDeletionSpan.Value.End <= this.totalSourceSize);
+ Debug.Assert(sourceInsertionPosition == null || sourceInsertionPosition >= 0);
+ Debug.Assert(sourceInsertionPosition == null || sourceInsertionPosition <= this.totalSourceSize);
+
+ ElisionMapNode newLeft = this.left;
+ ElisionMapNode newRight = this.right;
+ int newExposedLineBreakCount = this.exposedLineBreakCount;
+ int newSourceLineBreakCount = this.sourceLineBreakCount;
+ int newExposedSize = exposedSize;
+ int newSourceSize = sourceSize;
+ bool newLeftmostElision = this.leftmostElision;
+
+ int leftTotalSourceSize = LeftTotalSourceSize();
+
+ Span leftSourceSpan = new Span(0, leftTotalSourceSize);
+ Span midExposedSourceSpan = new Span(leftTotalSourceSize, this.exposedSize);
+ Span midHiddenSourceSpan = new Span(midExposedSourceSpan.End, this.sourceSize - this.exposedSize);
+ Span rightSourceSpan = new Span(midHiddenSourceSpan.End, this.totalSourceSize - leftTotalSourceSize - this.sourceSize);
+
+ #region Incorporate left subtree changes
+ Span? leftSourceDeletionSpan = leftSourceSpan.Overlap(sourceDeletionSpan);
+ bool insertionOnLeft = sourceInsertionPosition.HasValue && sourceInsertionPosition.Value < leftTotalSourceSize;
+ int? leftSourceInsertionPosition = null;
+ if (insertionOnLeft)
+ {
+ if (leftSourceDeletionSpan.HasValue && leftSourceDeletionSpan.Value.End == midExposedSourceSpan.Start)
+ {
+ // we have a replacement, and the deleted text in the left subtree touches the left edge of the current node.
+ // The inserted text needs to be exposed in this node, not possibly swallowed into a hidden part of the left node.
+ // Leave the leftSourceInsertionPosition equal to null so the left subtree doesn't stick it at the end of itself
+ insertionOnLeft = false;
+ sourceInsertionPosition = midExposedSourceSpan.Start;
+ Debug.Assert(!this.leftmostElision); // there is no node to the left of a leftmost elision node
+ }
+ else
+ {
+ leftSourceInsertionPosition = sourceInsertionPosition;
+ }
+ }
+ if (insertionOnLeft || leftSourceDeletionSpan.HasValue)
+ {
+ // insertion (if any) and start of deletion (if any) is in the left subtree
+ newLeft = this.left.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot,
+ afterSourceSnapshot : afterSourceSnapshot,
+ beforeElisionSnapshot : beforeElisionSnapshot,
+ sourceInsertionPosition : leftSourceInsertionPosition,
+ newText : newText,
+ sourceDeletionSpan : leftSourceSpan.Overlap(sourceDeletionSpan),
+ absoluteSourceOldPosition : absoluteSourceOldPosition,
+ absoluteSourceNewPosition : absoluteSourceNewPosition,
+ projectedPrefixSize : projectedPrefixSize,
+ projectedChanges : projectedChanges,
+ incomingAccumulatedDelta : incomingAccumulatedDelta,
+ outgoingAccumulatedDelta : ref outgoingAccumulatedDelta,
+ accumulatedDelete : ref accumulatedDelete);
+ }
+ #endregion
+
+ #region Incorporate current subtree changes
+
+ Span? exposedSourceDeletion = midExposedSourceSpan.Overlap(sourceDeletionSpan);
+ Span? hiddenSourceDeletion = midHiddenSourceSpan.Overlap(sourceDeletionSpan);
+
+ // Insertion and deletion are handled independently and recombined by text change normalization
+ // Insertion
+
+ // In each of the three cases below, if an insertion belongs in this node, set sourceInsertionPosition to
+ // null, preventing it from also being performed in the right subtree when the current node has size zero.
+ if (sourceInsertionPosition.HasValue)
+ {
+ // special case for leftmostElision node
+ if (this.leftmostElision)
+ {
+ // insertion into the exposed part of this node isn't possible
+ Debug.Assert(this.left == null);
+ Debug.Assert(this.exposedSize == 0);
+ Debug.Assert(leftTotalSourceSize == 0);
+ if (sourceInsertionPosition.Value <= this.sourceSize)
+ {
+ // insertion into hidden prefix of elision buffer
+ newSourceSize += newText.Length;
+
+ int incrementalLineCount;
+ ComputeIncrementalLineCountForHiddenInsertion(afterSourceSnapshot, absoluteSourceNewPosition, newText, out incrementalLineCount);
+
+ newSourceLineBreakCount += incrementalLineCount;
+ sourceInsertionPosition = null;
+ }
+ }
+ else if ((leftTotalSourceSize <= sourceInsertionPosition.Value) &&
+ (sourceInsertionPosition.Value <= leftTotalSourceSize + this.exposedSize))
+ {
+ // insertion (if any) is in the exposed part of the current node
+ newExposedSize += newText.Length;
+ newSourceSize += newText.Length;
+ int projectedPosition = projectedPrefixSize + sourceInsertionPosition.Value - LeftTotalHiddenSize() - incomingAccumulatedDelta;
+
+ // effects on line count are computed based on local information. Interactions with adjacent segments
+ // must be handled at a higher level (undone).
+
+ int deletionLength = 0;
+ if (exposedSourceDeletion.HasValue)
+ {
+ Debug.Assert(exposedSourceDeletion.Value.Start == sourceInsertionPosition.Value);
+ deletionLength = exposedSourceDeletion.Value.Length;
+ }
+
+ char? predChar = sourceInsertionPosition.Value > midExposedSourceSpan.Start
+ ? afterSourceSnapshot[absoluteSourceNewPosition - 1]
+ : (char?)null; // insertion is at start of segment
+ char? succChar = sourceInsertionPosition.Value + deletionLength < midExposedSourceSpan.End
+ ? afterSourceSnapshot[absoluteSourceNewPosition + newText.Length]
+ : (char?)null; // insertion is at end of segment
+
+ LineBreakBoundaryConditions boundaryConditions;
+ int incrementalLineCount;
+ ComputeIncrementalLineCountForExposedInsertion(predChar, succChar, newText, out incrementalLineCount, out boundaryConditions);
+
+ newExposedLineBreakCount += incrementalLineCount;
+ newSourceLineBreakCount += incrementalLineCount;
+
+ TextChange change = new TextChange(projectedPosition, StringRebuilder.Empty, newText, boundaryConditions);
+ projectedChanges.Add(change);
+ outgoingAccumulatedDelta += change.Delta;
+ sourceInsertionPosition = null;
+ }
+ else if ((leftTotalSourceSize + this.exposedSize < sourceInsertionPosition.Value) &&
+ ((sourceInsertionPosition.Value < leftTotalSourceSize + this.sourceSize) || (this.right == null)))
+ {
+ // insertion (if any) is in the hidden part of the current node
+ // Unless...we are also deleting from the point of the insertion through the end of the node, in which case
+ // the insertion should go at the beginning of the next segment. If there is no right subtree, this case will
+ // have been handled on the way down by some ancestor node.
+ if (this.right != null && (hiddenSourceDeletion.HasValue && hiddenSourceDeletion.Value.End == midHiddenSourceSpan.End))
+ {
+ sourceInsertionPosition = midHiddenSourceSpan.End;
+ }
+ else
+ {
+ newSourceSize += newText.Length;
+
+ int incrementalLineCount;
+ ComputeIncrementalLineCountForHiddenInsertion(afterSourceSnapshot, absoluteSourceNewPosition, newText, out incrementalLineCount);
+
+ newSourceLineBreakCount += incrementalLineCount;
+ sourceInsertionPosition = null;
+ }
+ }
+ }
+
+ // Deletion of exposed text
+ if (exposedSourceDeletion.HasValue)
+ {
+ newExposedSize -= exposedSourceDeletion.Value.Length;
+ newSourceSize -= exposedSourceDeletion.Value.Length;
+ int projectedDeletionPosition = projectedPrefixSize + exposedSourceDeletion.Value.Start - LeftTotalHiddenSize();
+ int sourceDeletionSegmentPosition = absoluteSourceOldPosition - accumulatedDelete;
+ StringRebuilder exposedDeletionText =
+ BufferFactoryService.StringRebuilderFromSnapshotAndSpan(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, exposedSourceDeletion.Value.Length));
+
+ LineBreakBoundaryConditions boundaryConditions;
+ int incrementalLineCount;
+ ComputeIncrementalLineCountForDeletion(beforeElisionSnapshot, new Span(projectedDeletionPosition - incomingAccumulatedDelta, exposedSourceDeletion.Value.Length), exposedDeletionText, out incrementalLineCount, out boundaryConditions);
+
+ newExposedLineBreakCount += incrementalLineCount;
+ newSourceLineBreakCount += incrementalLineCount;
+ TextChange change = new TextChange(projectedDeletionPosition - incomingAccumulatedDelta, exposedDeletionText, StringRebuilder.Empty, boundaryConditions);
+ projectedChanges.Add(change);
+ outgoingAccumulatedDelta += change.Delta;
+ accumulatedDelete += change.Delta;
+ }
+
+ // Deletion of hidden text
+ if (hiddenSourceDeletion.HasValue)
+ {
+ int sourceDeletionSegmentPosition = absoluteSourceOldPosition - accumulatedDelete;
+ StringRebuilder hiddenDeletionText =
+ BufferFactoryService.StringRebuilderFromSnapshotAndSpan(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, hiddenSourceDeletion.Value.Length));
+
+ LineBreakBoundaryConditions dontCare;
+ int incrementalLineCount;
+ ComputeIncrementalLineCountForDeletion(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, hiddenSourceDeletion.Value.Length), hiddenDeletionText, out incrementalLineCount, out dontCare);
+ newSourceLineBreakCount += incrementalLineCount;
+
+ newSourceSize -= hiddenSourceDeletion.Value.Length;
+ accumulatedDelete -= hiddenSourceDeletion.Value.Length;
+ }
+ #endregion
+
+ #region Incorporate right subtree changes
+ Span? rightSourceDeletionSpan = rightSourceSpan.Overlap(sourceDeletionSpan);
+ bool insertionOnRight = (sourceInsertionPosition.HasValue) && (this.right != null) && (leftTotalSourceSize + this.sourceSize <= sourceInsertionPosition.Value);
+ if (rightSourceDeletionSpan.HasValue || insertionOnRight)
+ {
+ // insertion (if any) or part of deletion (if any) is in the right subtree
+ newRight = this.right.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot,
+ afterSourceSnapshot : afterSourceSnapshot,
+ beforeElisionSnapshot : beforeElisionSnapshot,
+ sourceInsertionPosition : insertionOnRight ? sourceInsertionPosition.Value - leftTotalSourceSize - this.sourceSize : (int?)null,
+ newText : insertionOnRight ? newText : StringRebuilder.Empty,
+ sourceDeletionSpan : rightSourceDeletionSpan.HasValue
+ ? new Span(rightSourceDeletionSpan.Value.Start - (leftTotalSourceSize + this.sourceSize),
+ rightSourceDeletionSpan.Value.Length)
+ : (Span?)null,
+ absoluteSourceOldPosition : absoluteSourceOldPosition,
+ absoluteSourceNewPosition : absoluteSourceNewPosition,
+ projectedPrefixSize : projectedPrefixSize + LeftTotalExposedSize() + this.exposedSize,
+ projectedChanges : projectedChanges,
+ incomingAccumulatedDelta : incomingAccumulatedDelta,
+ outgoingAccumulatedDelta : ref outgoingAccumulatedDelta,
+ accumulatedDelete : ref accumulatedDelete);
+ }
+ #endregion
+
+ return new ElisionMapNode(newExposedSize, newSourceSize, newExposedLineBreakCount, newSourceLineBreakCount, newLeft, newRight, newLeftmostElision);
+ }
+
+ private static void ComputeIncrementalLineCountForHiddenInsertion(ITextSnapshot afterSnapshot,
+ int start,
+ StringRebuilder insertedText,
+ out int incrementalLineCount)
+ {
+ int lineCount = insertedText.LineBreakCount;
+ LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None;
+
+ Debug.Assert(start >= 0 && start <= afterSnapshot.Length);
+ Debug.Assert(insertedText.Length > 0);
+
+ if (start > 0 && afterSnapshot[start - 1] == '\r')
+ {
+ bc = bc | LineBreakBoundaryConditions.PrecedingReturn;
+ if (insertedText.FirstCharacter == '\n')
+ {
+ // \n was inserted after \r, and we counted it above as a new line, which it isn't.
+ // correct for that.
+ lineCount--;
+ }
+ }
+
+ int end = start + insertedText.Length;
+ if (end < afterSnapshot.Length && afterSnapshot[end] == '\n')
+ {
+ bc = bc | LineBreakBoundaryConditions.SucceedingNewline;
+ if (insertedText.LastCharacter == '\r')
+ {
+ // \r was inserted before \n, and we counted it above as a new line, which it isn't.
+ // correct for that.
+ lineCount--;
+ }
+ }
+ if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline))
+ {
+ // the insertion separated a \r and a \n, so increase the line count
+ lineCount++;
+ }
+ incrementalLineCount = lineCount;
+ }
+
+ /// <summary>
+ /// Determine the impact of an insertion on the line count.
+ /// </summary>
+ private static void ComputeIncrementalLineCountForExposedInsertion(char? predecessor,
+ char? successor,
+ StringRebuilder insertedText,
+ out int incrementalLineCount,
+ out LineBreakBoundaryConditions boundaryConditions)
+ {
+ int lineCount = insertedText.LineBreakCount;
+ LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None;
+
+ if (predecessor == '\r')
+ {
+ bc = bc | LineBreakBoundaryConditions.PrecedingReturn;
+ if (insertedText.FirstCharacter == '\n')
+ {
+ // \n was inserted after \r, and we counted it above as a new line, which it isn't.
+ // correct for that.
+ lineCount--;
+ }
+ }
+ if (successor == '\n')
+ {
+ bc = bc | LineBreakBoundaryConditions.SucceedingNewline;
+ if (insertedText.LastCharacter == '\r')
+ {
+ // \r was inserted before \n, and we counted it above as a new line, which it isn't.
+ // correct for that.
+ lineCount--;
+ }
+ }
+ if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline))
+ {
+ // the insertion separated a \r and a \n, so increase the line count
+ lineCount++;
+ }
+ incrementalLineCount = lineCount;
+ boundaryConditions = bc;
+ }
+
+ /// <summary>
+ /// Determine the impact of a deletion on the line count.
+ /// </summary>
+ private static void ComputeIncrementalLineCountForDeletion(ITextSnapshot beforeSnapshot,
+ Span deletionSpan,
+ StringRebuilder deletedText,
+ out int incrementalLineCount,
+ out LineBreakBoundaryConditions boundaryConditions)
+ {
+ int lineCount = -deletedText.LineBreakCount;
+ LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None;
+
+ Debug.Assert(deletionSpan.End <= beforeSnapshot.Length);
+ Debug.Assert(deletedText.Length > 0);
+
+ if (deletionSpan.Start > 0 && beforeSnapshot[deletionSpan.Start - 1] == '\r')
+ {
+ bc = bc | LineBreakBoundaryConditions.PrecedingReturn;
+ if (deletedText[0] == '\n')
+ {
+ // the \n of a \r\n pair was deleted and we counted it as losing a line, which it isn't.
+ // correct for that.
+ lineCount++;
+ }
+ }
+ if (deletionSpan.End < beforeSnapshot.Length && beforeSnapshot[deletionSpan.End] == '\n')
+ {
+ bc = bc | LineBreakBoundaryConditions.SucceedingNewline;
+ if (deletedText[deletedText.Length - 1] == '\r')
+ {
+ // the \r of a \r\n pair was deleted and we counted it as losing a line, which it isn't.
+ // correct for that.
+ lineCount++;
+ }
+ }
+ if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline))
+ {
+ // the deletion joined a \r and a \n, so decrease the line count
+ lineCount--;
+ }
+ incrementalLineCount = lineCount;
+ boundaryConditions = bc;
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs
new file mode 100644
index 0000000..36b0779
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs
@@ -0,0 +1,262 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Strings = Microsoft.VisualStudio.Text.Implementation.Strings;
+
+ internal class ElisionSnapshot : BaseProjectionSnapshot, IProjectionSnapshot, IElisionSnapshot
+ {
+ #region State and Construction
+ private readonly ElisionBuffer elisionBuffer;
+ private readonly ITextSnapshot sourceSnapshot;
+ private readonly ReadOnlyCollection<ITextSnapshot> sourceSnapshots;
+ private readonly ElisionMap content;
+ private readonly bool fillInMappingMode;
+
+ public ElisionSnapshot(ElisionBuffer elisionBuffer,
+ ITextSnapshot sourceSnapshot,
+ ITextVersion2 version,
+ StringRebuilder builder,
+ ElisionMap content,
+ bool fillInMappingMode)
+ : base(version, builder)
+ {
+ this.elisionBuffer = elisionBuffer;
+ this.sourceSnapshot = sourceSnapshot;
+ // The SourceSnapshots property is used heavily, so cache a handy copy.
+ this.sourceSnapshots = new ReadOnlyCollection<ITextSnapshot>(new FrugalList<ITextSnapshot>() { sourceSnapshot });
+ this.totalLength = content.Length;
+ this.content = content;
+ this.totalLineCount = content.LineCount;
+ this.fillInMappingMode = fillInMappingMode;
+ Debug.Assert(this.totalLength == version.Length,
+ string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "Elision Snapshot Inconsistency. Content: {0}, Previous + delta: {1}", this.totalLength, version.Length));
+ if (this.totalLength != version.Length)
+ {
+ throw new InvalidOperationException(Strings.InvalidLengthCalculation);
+ }
+ }
+ #endregion
+
+ #region Buffers and Spans
+ public override IProjectionBufferBase TextBuffer
+ {
+ get { return this.elisionBuffer; }
+ }
+
+ IElisionBuffer IElisionSnapshot.TextBuffer
+ {
+ get { return this.elisionBuffer; }
+ }
+
+ protected override ITextBuffer TextBufferHelper
+ {
+ get { return this.elisionBuffer; }
+ }
+
+ public override int SpanCount
+ {
+ get { return this.content.SpanCount; }
+ }
+
+ public override ReadOnlyCollection<ITextSnapshot> SourceSnapshots
+ {
+ get { return this.sourceSnapshots; }
+ }
+
+ public ITextSnapshot SourceSnapshot
+ {
+ get { return this.sourceSnapshot; }
+ }
+
+ public SnapshotPoint MapFromSourceSnapshotToNearest(SnapshotPoint point)
+ {
+ return this.content.MapFromSourceSnapshotToNearest(this, point.Position);
+ }
+
+ public override ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ return this.sourceSnapshot.TextBuffer == textBuffer ? this.sourceSnapshot : null;
+ }
+
+ public override ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ if (this.sourceSnapshot.TextBuffer == textBuffer)
+ {
+ return this.sourceSnapshot;
+ }
+
+ IProjectionSnapshot2 projSnap = this.sourceSnapshot as IProjectionSnapshot2;
+ if (projSnap != null)
+ {
+ return projSnap.GetMatchingSnapshotInClosure(textBuffer);
+ }
+
+ return null;
+ }
+
+ public override ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+
+ if (match(this.sourceSnapshot.TextBuffer))
+ {
+ return this.sourceSnapshot;
+ }
+
+ IProjectionSnapshot2 projSnap = this.sourceSnapshot as IProjectionSnapshot2;
+ if (projSnap != null)
+ {
+ return projSnap.GetMatchingSnapshotInClosure(match);
+ }
+
+ return null;
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count)
+ {
+ if (startSpanIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException("startSpanIndex");
+ }
+ if (count < 0 || startSpanIndex + count > SpanCount)
+ {
+ throw new ArgumentOutOfRangeException("count");
+ }
+ return new ReadOnlyCollection<SnapshotSpan>(this.content.GetSourceSpans(this.sourceSnapshot, startSpanIndex, count));
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans()
+ {
+ return GetSourceSpans(0, this.content.SpanCount);
+ }
+ #endregion
+
+ #region Mapping
+ public override SnapshotPoint MapToSourceSnapshot(int position)
+ {
+ if (position < 0 || position > this.totalLength)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ FrugalList<SnapshotPoint> points = this.content.MapInsertionPointToSourceSnapshots(this, position);
+ if (points.Count == 1)
+ {
+ return points[0];
+ }
+ else if (this.elisionBuffer.resolver == null)
+ {
+ return points[points.Count - 1];
+ }
+ else
+ {
+ return points[this.elisionBuffer.resolver.GetTypicalInsertionPosition(new SnapshotPoint(this, position), new ReadOnlyCollection<SnapshotPoint>(points))];
+ }
+ }
+
+ public override SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity)
+ {
+ if (position < 0 || position > this.totalLength)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+ return this.content.MapToSourceSnapshot(this.sourceSnapshot, position, affinity);
+ }
+
+ public override SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint point, PositionAffinity affinity)
+ {
+ if (point.Snapshot != this.sourceSnapshot)
+ {
+ throw new ArgumentException("The point does not belong to a source snapshot of the projection snapshot");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+ return this.content.MapFromSourceSnapshot(this, point.Position);
+ }
+
+ private ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span, bool fillIn)
+ {
+ if (span.End > this.totalLength)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+ FrugalList<SnapshotSpan> result = new FrugalList<SnapshotSpan>();
+ if (fillIn)
+ {
+ this.content.MapToSourceSnapshotsInFillInMode(this.sourceSnapshot, span, result);
+ }
+ else
+ {
+ this.content.MapToSourceSnapshots(this, span, result);
+ }
+ return new ReadOnlyCollection<SnapshotSpan>(result);
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span)
+ {
+ return MapToSourceSnapshots(span, this.fillInMappingMode);
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span)
+ {
+ return MapToSourceSnapshots(span, false);
+ }
+
+ public override ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan span)
+ {
+ if (span.Snapshot != this.sourceSnapshot)
+ {
+ throw new ArgumentException("The span does not belong to a source snapshot of the projection snapshot");
+ }
+ FrugalList<Span> result = new FrugalList<Span>();
+ this.content.MapFromSourceSnapshot(span, result);
+ return new ReadOnlyCollection<Span>(result);
+ }
+
+ internal override ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer)
+ {
+ return new ReadOnlyCollection<SnapshotPoint>(this.content.MapInsertionPointToSourceSnapshots(this, position));
+ }
+
+ internal override ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer)
+ {
+ // this implementation won't return zero-length spans on the edges as it
+ // should, but that's OK because it is not called in Beta1 (we never edit
+ // elision buffers directly). Third parties might do so, so we need a non-throwing
+ // implementation here. Zero-length spans will be added for Beta2.
+ return MapToSourceSnapshots(replacementSpan, false);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs
new file mode 100644
index 0000000..67b747e
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs
@@ -0,0 +1,1914 @@
+//
+// 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.Projection.Implementation
+{
+ // todo: denormalizations that are performed may possibly be removed by exploiting OldPosition
+ // information in TextChange. Investigate this.
+
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using System.Text;
+
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Text.Differencing;
+
+ using Strings = Microsoft.VisualStudio.Text.Implementation.Strings;
+
+ internal sealed partial class ProjectionBuffer : BaseProjectionBuffer, IProjectionBuffer
+ {
+ #region ProjectionEdit Class
+ private class ProjectionEdit : Edit, ISubordinateTextEdit
+ {
+ private ProjectionBuffer projectionBuffer;
+ private bool subordinate;
+
+ public ProjectionEdit(ProjectionBuffer projectionBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag)
+ : base(projectionBuffer, originSnapshot, options, reiteratedVersionNumber, editTag)
+ {
+ this.projectionBuffer = projectionBuffer;
+ this.subordinate = true;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.projectionBuffer; }
+ }
+
+ // this is the master edit path -- initiated from outside
+ protected override ITextSnapshot PerformApply()
+ {
+ CheckActive();
+ this.applied = true;
+ this.subordinate = false;
+
+ ITextSnapshot result = this.baseBuffer.currentSnapshot;
+
+ if (this.changes.Count > 0)
+ {
+ this.projectionBuffer.group.PerformMasterEdit(this.projectionBuffer, this, this.options, this.editTag);
+
+ if (!this.Canceled)
+ {
+ result = this.baseBuffer.currentSnapshot;
+ }
+ }
+ else
+ {
+ // vacuous edit
+ this.baseBuffer.editInProgress = false;
+ }
+
+ return result;
+ }
+
+ public void PreApply()
+ {
+ // called for all non-vacuous edits
+ if (this.changes.Count > 0)
+ {
+ this.projectionBuffer.ComputeSourceEdits(this.changes);
+ }
+ }
+
+ public void FinalApply() // TODO: make FinalApply return event raisers, eliminate FinishEdit()
+ {
+ // called for all non-vacuous edits
+ if (this.changes.Count > 0 || this.projectionBuffer.pendingContentChangedEventArgs.Count > 0)
+ {
+ this.projectionBuffer.group.CancelIndependentEdit(this.projectionBuffer); // just in case
+ IList<ITextEventRaiser> eventRaisers = this.projectionBuffer.InterpretSourceChanges(this.options, /*this.reiteratedVersionNumber,*/ this.editTag);
+ this.projectionBuffer.group.EnqueueEvents(eventRaisers, this.baseBuffer);
+
+ // raise immediate events
+ foreach (var raiser in eventRaisers)
+ {
+ raiser.RaiseEvent(this.baseBuffer, true);
+ }
+ }
+
+ this.projectionBuffer.editInProgress = false;
+ if (this.subordinate)
+ {
+ this.baseBuffer.group.FinishEdit();
+ }
+ }
+
+ public override void CancelApplication()
+ {
+ if (!this.canceled)
+ {
+ base.CancelApplication();
+ this.projectionBuffer.editApplicationInProgress = false;
+ this.projectionBuffer.pendingContentChangedEventArgs.Clear();
+ }
+ }
+ }
+ #endregion
+
+ #region SourceBufferSet Class
+ /// <summary>
+ /// Tracks the Source TextBuffers of this ProjectionBuffer. There is exactly one SourceBufferSet
+ /// per ProjectionBufferImpl.
+ /// </summary>
+ private class SourceBufferSet
+ {
+ private class BufferTracker
+ {
+ public ITextBuffer _buffer;
+ public int _spanCount;
+ public BufferTracker(ITextBuffer buffer)
+ {
+ _buffer = buffer;
+ _spanCount = 1;
+ }
+ }
+
+ // presumption: the number of source buffers is small
+ private bool _inTransaction;
+ private List<BufferTracker> _sourceBufferTrackers = new List<BufferTracker>();
+ private FrugalList<ITextBuffer> _addedBuffers;
+ private FrugalList<ITextBuffer> _removedBuffers;
+
+ private int Find(ITextBuffer buffer)
+ {
+ for (int i = 0; i < _sourceBufferTrackers.Count; ++i)
+ {
+ if (buffer == _sourceBufferTrackers[i]._buffer)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public List<ITextBuffer> SourceBuffers
+ {
+ get
+ {
+ List<ITextBuffer> sourceBuffers = new List<ITextBuffer>();
+ foreach (BufferTracker bt in _sourceBufferTrackers)
+ {
+ if (!sourceBuffers.Contains(bt._buffer))
+ {
+ sourceBuffers.Add(bt._buffer);
+ }
+ }
+ return sourceBuffers;
+ }
+ }
+
+ public void StartTransaction()
+ {
+ Debug.Assert(!_inTransaction);
+ _addedBuffers = new FrugalList<ITextBuffer>();
+ _removedBuffers = new FrugalList<ITextBuffer>();
+ _inTransaction = true;
+ }
+
+ public void FinishTransaction(out FrugalList<ITextBuffer> addedBuffers, out FrugalList<ITextBuffer> removedBuffers)
+ {
+ Debug.Assert(_inTransaction);
+
+ // If a buffer was removed and then added, eliminate it from both lists.
+ // Since these lists should be extremely short, nothing fancy here.
+ FrugalList<ITextBuffer> comingAndGoingBuffers = new FrugalList<ITextBuffer>();
+ foreach (ITextBuffer buffer in _addedBuffers)
+ {
+ if (_removedBuffers.Contains(buffer))
+ {
+ comingAndGoingBuffers.Add(buffer);
+ }
+ }
+ foreach (ITextBuffer buffer in comingAndGoingBuffers)
+ {
+ _addedBuffers.Remove(buffer);
+ _removedBuffers.Remove(buffer);
+ }
+
+ addedBuffers = _addedBuffers;
+ removedBuffers = _removedBuffers;
+ _addedBuffers = null;
+ _removedBuffers = null;
+ _inTransaction = false;
+ }
+
+ public void AddSpan(ITrackingSpan span)
+ {
+ Debug.Assert(_inTransaction);
+ int i = Find(span.TextBuffer);
+ if (i < 0)
+ {
+ _sourceBufferTrackers.Add(new BufferTracker(span.TextBuffer));
+ _addedBuffers.Add(span.TextBuffer);
+ }
+ else
+ {
+ _sourceBufferTrackers[i]._spanCount++;
+ }
+ }
+
+ public void RemoveSpan(ITrackingSpan span)
+ {
+ Debug.Assert(_inTransaction);
+ int i = Find(span.TextBuffer);
+ Debug.Assert(i >= 0);
+ if (--_sourceBufferTrackers[i]._spanCount == 0)
+ {
+ _sourceBufferTrackers.RemoveAt(i);
+ _removedBuffers.Add(span.TextBuffer);
+ }
+ }
+ }
+
+ public override IList<ITextBuffer> SourceBuffers
+ {
+ // this is problematic, but we need it until the buffer graph implementation catches up to the new world
+ get
+ {
+ return this.sourceBufferSet.SourceBuffers;
+ }
+ }
+ #endregion
+
+ #region Private State
+ private ProjectionBufferOptions bufferOptions;
+ private IDifferenceService differenceService;
+ private List<ITrackingSpan> sourceSpans = new List<ITrackingSpan>();
+ private SourceBufferSet sourceBufferSet = new SourceBufferSet();
+ private ProjectionSnapshot currentProjectionSnapshot;
+
+ private IInternalTextBufferFactory textBufferFactory;
+ internal ITextBuffer literalBuffer;
+ private IReadOnlyRegion literalBufferRor;
+
+ private List<WeakEventHook> eventHooks = new List<WeakEventHook>();
+ #endregion
+
+ #region Construction
+ public ProjectionBuffer(IInternalTextBufferFactory textBufferFactory,
+ IProjectionEditResolver resolver,
+ IContentType contentType,
+ IList<object> initialSourceSpans,
+ IDifferenceService differenceService,
+ ITextDifferencingService textDifferencingService,
+ ProjectionBufferOptions options,
+ GuardedOperations guardedOperations)
+ : base(resolver, contentType, textDifferencingService, guardedOperations)
+ {
+ // Parameters are validated outside
+ Debug.Assert(initialSourceSpans != null);
+ this.textBufferFactory = textBufferFactory;
+ this.differenceService = differenceService;
+ this.bufferOptions = options;
+
+ SpanManager spanManager = new SpanManager(this, 0, 0, initialSourceSpans, false, false);
+ spanManager.PerformChecks();
+ spanManager.ProcessLiteralSpans();
+
+ int spanPos = 0;
+ this.sourceBufferSet.StartTransaction();
+ int initialLength = 0;
+ List<SnapshotSpan> snapshotSpans = new List<SnapshotSpan>();
+ foreach (ITrackingSpan initialTrackingSpan in spanManager.SpansToInsert)
+ {
+ AddSpan(spanPos++, initialTrackingSpan);
+ SnapshotSpan snapSpan = new SnapshotSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot,
+ initialTrackingSpan.GetSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot));
+ initialLength += snapSpan.Length;
+ snapshotSpans.Add(snapSpan);
+ }
+
+ StringRebuilder newBuilder = StringRebuilder.Empty;
+ for (int i = 0; (i < snapshotSpans.Count); ++i)
+ newBuilder = newBuilder.Append(BufferFactoryService.StringRebuilderFromSnapshotSpan(snapshotSpans[i]));
+ this.builder = newBuilder;
+
+ this.currentVersion.SetLength(initialLength); // this is a bit hacky
+
+ FrugalList<ITextBuffer> addedBuffers;
+ FrugalList<ITextBuffer> removedBuffers;
+ this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers);
+
+ // listen to changes to source buffers
+ bool firstAddedBuffer = true;
+ BufferGroup chosenGroup = null;
+ foreach (ITextBuffer addedBuffer in addedBuffers)
+ {
+ BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer;
+ if (firstAddedBuffer)
+ {
+ firstAddedBuffer = false;
+ chosenGroup = baseAddedBuffer.group;
+ chosenGroup.AddMember(this);
+ }
+ else
+ {
+ chosenGroup.Swallow(baseAddedBuffer.group);
+ }
+ if ((baseAddedBuffer != this.literalBuffer) || ((this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0))
+ {
+ this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer));
+ }
+ }
+ this.group = chosenGroup ?? new BufferGroup(this);
+
+ this.currentProjectionSnapshot = new ProjectionSnapshot(this, base.currentVersion, this.builder, snapshotSpans);
+ this.currentSnapshot = this.currentProjectionSnapshot;
+ }
+ #endregion
+
+ #region Span Editing and Management
+ private class SourceSpansChangedEventRaiser : ITextEventRaiser
+ {
+ ProjectionSourceSpansChangedEventArgs args;
+
+ public SourceSpansChangedEventRaiser(ProjectionSourceSpansChangedEventArgs args)
+ {
+ this.args = args;
+ }
+
+ public void RaiseEvent(BaseBuffer baseBuffer, bool immediate)
+ {
+ ProjectionBuffer projBuffer = (ProjectionBuffer)baseBuffer;
+ ProjectionSourceBuffersChangedEventArgs bufferArgs = args as ProjectionSourceBuffersChangedEventArgs;
+ if (bufferArgs != null)
+ {
+ EventHandler<ProjectionSourceBuffersChangedEventArgs> bufferHandlers =
+ immediate ? projBuffer.SourceBuffersChangedImmediate : projBuffer.SourceBuffersChanged;
+ if (bufferHandlers != null)
+ {
+ bufferHandlers(baseBuffer, bufferArgs);
+ }
+ }
+
+ EventHandler<ProjectionSourceSpansChangedEventArgs> spanHandlers =
+ immediate ? projBuffer.SourceSpansChangedImmediate : projBuffer.SourceSpansChanged;
+ if (spanHandlers != null)
+ {
+ spanHandlers(baseBuffer, args);
+ }
+
+ // now raise the text content changed event
+ baseBuffer.RawRaiseEvent(args, immediate);
+ }
+
+ public bool HasPostEvent
+ {
+ get { return true; }
+ }
+ }
+
+ /// <summary>
+ /// Perform validity checking and normalization of source spans that are to be inserted. Checks
+ /// for spans that are null, of the wrong type (neither string nor ITrackingSpan), would induce
+ /// projection buffer cycles, have the wrong tracking mode, or induce projection overlaps.
+ /// Also converts string literals to ITrackingSpans over the literal source buffer.
+ /// </summary>
+ private class SpanManager
+ {
+ public int Position { get; private set; }
+ public int SpansToDelete { get; private set; }
+ public List<object> RawSpansToInsert { get; private set; }
+
+ // The set of text buffers that have been previously visited in a cyclic dependency check
+ private Dictionary<ITextBuffer, bool> visitedBufferSet = new Dictionary<ITextBuffer, bool>();
+ private ProjectionBuffer projBuffer;
+ private List<ITrackingSpan> spansToInsert;
+ private LiteralBufferHelper lit;
+ private bool checkForCycles;
+
+ public SpanManager(ProjectionBuffer projBuffer, int position, int spansToDelete, IList<object> rawSpansToInsert, bool checkForCycles, bool groupEdit)
+ {
+ this.projBuffer = projBuffer;
+ this.checkForCycles = checkForCycles;
+ this.lit = new LiteralBufferHelper(projBuffer, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0, groupEdit);
+
+ this.Position = position;
+ this.SpansToDelete = spansToDelete;
+ this.RawSpansToInsert = new List<object>(rawSpansToInsert);
+ }
+
+ public List<ITrackingSpan> SpansToInsert
+ {
+ get
+ {
+ if (this.spansToInsert == null)
+ {
+ this.lit.FinishEdit();
+ this.spansToInsert = new List<ITrackingSpan>();
+ foreach (object rawSpan in this.RawSpansToInsert)
+ {
+ ITrackingSpan trackingSpan = rawSpan as ITrackingSpan;
+ if (trackingSpan == null)
+ {
+ trackingSpan = this.projBuffer.literalBuffer.CurrentSnapshot.CreateTrackingSpan((Span)rawSpan, SpanTrackingMode.EdgeExclusive, TrackingFidelityMode.Forward);
+ }
+ this.spansToInsert.Add(trackingSpan);
+ }
+ }
+ return this.spansToInsert;
+ }
+ }
+
+ private void CheckForSourceBufferCycle(ITextBuffer buffer)
+ {
+ // TODO shouldn't we load up the visitedbufferset with existing source buffers first? at least for perf.
+ if (!this.visitedBufferSet.ContainsKey(buffer))
+ {
+ if (buffer == this.projBuffer)
+ {
+ throw new ArgumentException(Strings.SourceBufferCycle);
+ }
+ this.visitedBufferSet.Add(buffer, true);
+ IProjectionBuffer p = buffer as ProjectionBuffer;
+ if (p != null)
+ {
+ foreach (ITextBuffer sourceBuffer in p.SourceBuffers)
+ {
+ CheckForSourceBufferCycle(sourceBuffer);
+ }
+ }
+ }
+ }
+
+ private static void CheckTrackingMode(ITrackingSpan spanToInsert)
+ {
+ if (spanToInsert.TrackingMode != SpanTrackingMode.EdgeExclusive && spanToInsert.TrackingMode != SpanTrackingMode.Custom)
+ {
+ ITextSnapshot snap = spanToInsert.TextBuffer.CurrentSnapshot;
+ Span span = spanToInsert.GetSpan(snap);
+ if (spanToInsert.TrackingMode == SpanTrackingMode.EdgeInclusive)
+ {
+ if (span.Start > 0 || span.End < snap.Length)
+ {
+ throw new ArgumentException(Strings.InvalidEdgeInclusiveSourceSpan);
+ }
+ }
+ else if (spanToInsert.TrackingMode == SpanTrackingMode.EdgePositive)
+ {
+ if (span.End < snap.Length)
+ {
+ throw new ArgumentException(Strings.InvalidEdgePositiveSourceSpan);
+ }
+ }
+ else if (span.Start > 0)
+ {
+ throw new ArgumentException(Strings.InvalidEdgeNegativeSourceSpan);
+ }
+ }
+ }
+
+ private IEnumerable<ITrackingSpan> GetProposedSpans()
+ {
+ for (int s = 0; s < this.Position; ++s)
+ {
+ yield return projBuffer.sourceSpans[s];
+ }
+ for (int s = this.Position + this.SpansToDelete; s < projBuffer.sourceSpans.Count; ++s)
+ {
+ yield return projBuffer.sourceSpans[s];
+ }
+ for (int s = 0; s < this.RawSpansToInsert.Count; ++s)
+ {
+ ITrackingSpan ts = this.RawSpansToInsert[s] as ITrackingSpan;
+ if (ts != null)
+ {
+ yield return ts;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Build a list of ultimate sources for the given span, looking through all projections
+ /// </summary>
+ /// <param name="span"></param>
+ /// <returns></returns>
+ private IList<SnapshotSpan> BaseSourceSpans(SnapshotSpan span)
+ {
+ List<SnapshotSpan> result = new List<SnapshotSpan>();
+ if (span.Snapshot.TextBuffer is IProjectionBuffer)
+ {
+ foreach (SnapshotSpan s in ((IProjectionSnapshot)span.Snapshot).MapToSourceSnapshots(span))
+ {
+ result.AddRange(BaseSourceSpans(s));
+ }
+ }
+ else
+ {
+ result.Add(span);
+ }
+ return result;
+ }
+
+ private void CheckOverlap()
+ {
+ Dictionary<ITextBuffer, List<Span>> bufferToSpans = new Dictionary<ITextBuffer, List<Span>>();
+ foreach (ITrackingSpan proposedSpan in GetProposedSpans())
+ {
+ // Look through all projections
+ IList<SnapshotSpan> baseSpans = BaseSourceSpans(proposedSpan.GetSpan(proposedSpan.TextBuffer.CurrentSnapshot));
+
+ // Group by source buffer
+ foreach (SnapshotSpan baseSpan in baseSpans)
+ {
+ List<Span> spans;
+ ITextBuffer buffer = baseSpan.Snapshot.TextBuffer;
+ if (!bufferToSpans.TryGetValue(buffer, out spans))
+ {
+ spans = new List<Span>();
+ bufferToSpans.Add(buffer, spans);
+ }
+ spans.Add(baseSpan);
+ }
+ }
+
+ foreach (KeyValuePair<ITextBuffer, List<Span>> bufferSpans in bufferToSpans)
+ {
+ if (bufferSpans.Value.Count > 1)
+ {
+ // sort and check for overlap.
+ // sort by start position, except if two spans start at the same position sort by
+ // end position so that a null span comes first
+ bufferSpans.Value.Sort(delegate(Span x, Span y) { return x.Start == y.Start ? x.End - y.End : x.Start - y.Start; });
+ for (int s = 1; s < bufferSpans.Value.Count; ++s)
+ {
+ if (bufferSpans.Value[s].Start < bufferSpans.Value[s - 1].End)
+ {
+ throw new ArgumentException(Strings.OverlappingSourceSpans);
+ }
+ }
+ }
+ }
+ }
+
+ public void PerformChecks()
+ {
+ foreach (object spanToInsert in this.RawSpansToInsert)
+ {
+ if (spanToInsert == null)
+ {
+ throw new ArgumentNullException("spansToInsert");
+ }
+ ITrackingSpan trackingSpanToInsert = spanToInsert as ITrackingSpan;
+ if (trackingSpanToInsert != null)
+ {
+ if (checkForCycles)
+ {
+ try
+ {
+ CheckForSourceBufferCycle(trackingSpanToInsert.TextBuffer);
+ }
+ catch (ArgumentException)
+ {
+ throw new ArgumentException(Strings.SourceBufferCycle);
+ }
+ }
+ if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) == 0)
+ {
+ CheckTrackingMode(trackingSpanToInsert);
+ }
+ }
+ else
+ {
+ if (!(spanToInsert is string))
+ {
+ throw new ArgumentException(Strings.NeitherSpanNorString);
+ }
+ }
+ }
+ CheckOverlap();
+ }
+
+ public void ProcessLiteralSpans()
+ {
+ // must do deletions first!
+ for (int d = this.Position; d < this.Position + this.SpansToDelete; ++d)
+ {
+ ITrackingSpan sourceSpan = this.projBuffer.sourceSpans[d];
+ if (sourceSpan.TextBuffer == this.projBuffer.literalBuffer)
+ {
+ this.lit.Delete(sourceSpan);
+ }
+ }
+
+ for (int r = 0; r < this.RawSpansToInsert.Count; ++r)
+ {
+ object rawSpan = this.RawSpansToInsert[r];
+ string literalSpan = rawSpan as string;
+ if (literalSpan != null)
+ {
+ // change the string into a Span indicating what the bounds of
+ // the span will be when it is ready
+ this.RawSpansToInsert[r] = this.lit.Append(literalSpan);
+ }
+ }
+ }
+ }
+
+ private class LiteralBufferHelper
+ {
+ private ProjectionBuffer projBuffer;
+ bool performedEdit;
+ bool readOnly;
+ bool groupEdit;
+ int totalInsertions;
+ int totalDeletions;
+ int insertionPoint;
+
+ public LiteralBufferHelper(ProjectionBuffer projBuffer, bool readOnly, bool groupEdit)
+ {
+ this.projBuffer = projBuffer;
+ this.readOnly = readOnly;
+ this.groupEdit = groupEdit;
+ if (this.projBuffer.literalBuffer != null)
+ {
+ this.insertionPoint = this.projBuffer.literalBuffer.CurrentSnapshot.Length;
+ }
+ }
+
+ private void PrepareEdit()
+ {
+ if (this.projBuffer.literalBuffer == null)
+ {
+ this.projBuffer.literalBuffer =
+ projBuffer.textBufferFactory.CreateTextBuffer(string.Empty, projBuffer.textBufferFactory.InertContentType, readOnly);
+ this.insertionPoint = 0;
+ }
+ else if (this.projBuffer.literalBufferRor != null)
+ {
+ using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit())
+ {
+ rorEdit.RemoveReadOnlyRegion(this.projBuffer.literalBufferRor);
+ rorEdit.Apply();
+ }
+ this.projBuffer.literalBufferRor = null;
+ }
+ this.performedEdit = true;
+ }
+
+ public Span Append(string text)
+ {
+ PrepareEdit();
+ Span result;
+ if (this.groupEdit)
+ {
+ ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None);
+ edit.Insert(this.insertionPoint, text);
+ result = new Span(this.insertionPoint + this.totalInsertions - this.totalDeletions, text.Length);
+ this.totalInsertions += text.Length;
+ }
+ else
+ {
+ ITextSnapshot literalSnap = projBuffer.literalBuffer.CurrentSnapshot;
+ int length = literalSnap.Length;
+ literalSnap = projBuffer.literalBuffer.Insert(length, text);
+ result = new Span(length, text.Length);
+ }
+ return result;
+ }
+
+ public void Delete(ITrackingSpan trackingSpan)
+ {
+ PrepareEdit();
+ Span span = trackingSpan.GetSpan(projBuffer.literalBuffer.CurrentSnapshot);
+ if (this.groupEdit)
+ {
+ ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None);
+ edit.Delete(span);
+ totalDeletions += span.Length;
+ }
+ else
+ {
+ projBuffer.literalBuffer.Delete(span);
+ }
+ }
+
+ public void FinishEdit()
+ {
+ if (this.performedEdit && this.readOnly)
+ {
+ if (this.projBuffer.literalBuffer != null)
+ {
+ Debug.Assert(this.projBuffer.literalBufferRor == null);
+ using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit())
+ {
+ this.projBuffer.literalBufferRor =
+ rorEdit.CreateReadOnlyRegion(new Span(0, rorEdit.Snapshot.Length), SpanTrackingMode.EdgeInclusive, EdgeInsertionMode.Deny);
+ rorEdit.Apply();
+ }
+ }
+ }
+ }
+ }
+
+ private class SpanEdit : TextBufferBaseEdit, ISubordinateTextEdit
+ {
+ private ProjectionBuffer projBuffer;
+ private EditOptions editOptions = EditOptions.None;
+ private object tag = null;
+ private SpanManager spanManager;
+
+ public SpanEdit(ProjectionBuffer projBuffer) : base(projBuffer)
+ {
+ this.projBuffer = projBuffer;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.projBuffer; }
+ }
+
+ public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList<object> spansToInsert, EditOptions options, object editTag)
+ {
+ if (position < 0 || position > this.projBuffer.sourceSpans.Count)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (spansToReplace < 0 || position + spansToReplace > this.projBuffer.sourceSpans.Count)
+ {
+ throw new ArgumentOutOfRangeException("spansToReplace");
+ }
+ if (spansToInsert == null)
+ {
+ throw new ArgumentNullException("spansToInsert");
+ }
+
+ this.spanManager = new SpanManager(this.projBuffer, position, spansToReplace, spansToInsert, true, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0);
+ this.editOptions = options;
+ this.tag = editTag;
+
+ this.spanManager.PerformChecks();
+
+ // we are committed!
+
+ this.applied = true;
+ if (this.spanManager.SpansToDelete > 0 || this.spanManager.RawSpansToInsert.Count > 0)
+ {
+ // non-vacuous
+ this.projBuffer.group.PerformMasterEdit(this.projBuffer, this, this.editOptions, this.tag);
+ }
+ this.baseBuffer.group.FinishEdit();
+ this.baseBuffer.editInProgress = false;
+ return this.projBuffer.currentProjectionSnapshot;
+ }
+
+ public void PreApply()
+ {
+ this.projBuffer.editApplicationInProgress = true;
+ this.spanManager.ProcessLiteralSpans();
+ }
+
+ public void FinalApply()
+ {
+ ProjectionSourceSpansChangedEventArgs args = this.projBuffer.ApplySpanChanges(this.spanManager.Position, this.spanManager.SpansToDelete, this.spanManager.SpansToInsert, this.editOptions, this.tag);
+ SourceSpansChangedEventRaiser raiser = new SourceSpansChangedEventRaiser(args);
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ raiser.RaiseEvent(this.baseBuffer, true);
+ this.baseBuffer.editInProgress = false;
+ this.projBuffer.editApplicationInProgress = false;
+
+ if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0)
+ {
+ // the only pending changes should be changes to our literal buffer
+ Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count <= 1);
+ if (this.projBuffer.pendingContentChangedEventArgs.Count == 1)
+ {
+ Debug.Assert(this.projBuffer.pendingContentChangedEventArgs[0].Before.TextBuffer == this.projBuffer.literalBuffer);
+ }
+ // forget about changes to our literal buffer; we've already incorporated them
+ this.projBuffer.pendingContentChangedEventArgs.Clear();
+ }
+ else
+ {
+ Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count == 0);
+ }
+ }
+
+ public bool CheckForCancellation(Action cancelAction)
+ {
+ return true;
+ }
+
+ public override string ToString()
+ {
+ StringBuilder insertions = new StringBuilder();
+ for (int t = 0; t < this.spanManager.RawSpansToInsert.Count; ++t)
+ {
+ ITrackingSpan insertion = this.spanManager.RawSpansToInsert[t] as ITrackingSpan;
+ if (insertion != null)
+ {
+ insertions.Append(insertion.ToString());
+ }
+ else
+ {
+ insertions.Append("(Literal)");
+ }
+ if (t < this.spanManager.RawSpansToInsert.Count - 1)
+ {
+ insertions.Append(",");
+ }
+ }
+ return string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "pos: {0}, delete: {1}, insert: {2}",
+ this.spanManager.Position, this.spanManager.SpansToDelete, insertions.ToString());
+ }
+
+ public void RecordMasterChangeOffset(int masterChangeOffset)
+ {
+ throw new InvalidOperationException("Projection span edits shouldn't have change offsets.");
+ }
+ }
+
+ private ProjectionSourceSpansChangedEventArgs ApplySpanChanges(int position, int spansToDelete, IList<ITrackingSpan> spansToInsert, EditOptions options, object editTag)
+ {
+ ProjectionSnapshot beforeSnapshot = this.currentProjectionSnapshot;
+
+ List<ITrackingSpan> deletedSpans = new List<ITrackingSpan>();
+
+ List<SnapshotSpan> insertedSnapSpans = new List<SnapshotSpan>();
+ List<SnapshotSpan> deletedSnapSpans = new List<SnapshotSpan>();
+
+ this.sourceBufferSet.StartTransaction();
+
+ for (int i = position + spansToDelete - 1; i >= position; --i)
+ {
+ ITrackingSpan removedSpan = RemoveSpan(i);
+ deletedSpans.Insert(0, removedSpan); // preserve order!
+ deletedSnapSpans.Insert(0, this.currentProjectionSnapshot.GetSourceSpan(i));
+ }
+
+ int insertPosition = position;
+ foreach (ITrackingSpan span in spansToInsert)
+ {
+ AddSpan(insertPosition++, span);
+ insertedSnapSpans.Add(span.GetSpan(span.TextBuffer.CurrentSnapshot));
+ }
+
+ FrugalList<ITextBuffer> addedBuffers;
+ FrugalList<ITextBuffer> removedBuffers;
+ this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers);
+
+ // todo make this transactional in case it fails here, or else check thread affinity earlier
+ // todo combine with later loop (can it move up here?)
+ foreach (ITextBuffer addedBuffer in addedBuffers)
+ {
+ BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer;
+ this.group.Swallow(baseAddedBuffer.group);
+ }
+
+ int textPosition = 0;
+ for (int i = 0; i < position; ++i)
+ {
+ textPosition += this.currentProjectionSnapshot.GetSourceSpan(i).Length;
+ }
+
+ INormalizedTextChangeCollection normalizedChanges;
+ if (options.ComputeMinimalChange)
+ {
+ normalizedChanges = ComputeTextChangesByStringDiffing(options.DifferenceOptions, textPosition, deletedSnapSpans, insertedSnapSpans);
+ }
+ else
+ {
+ normalizedChanges = ComputeTextChangesBySpanDiffing(textPosition, deletedSnapSpans, insertedSnapSpans);
+ }
+
+ SetCurrentVersionAndSnapshot(normalizedChanges);
+
+ ProjectionSourceSpansChangedEventArgs args = null;
+ if (addedBuffers.Count > 0 || removedBuffers.Count > 0)
+ {
+ // Adjust buffer change listening
+ foreach (ITextBuffer addedBuffer in addedBuffers)
+ {
+ if (addedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0)
+ {
+ BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer;
+ this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer));
+ }
+ }
+ foreach (ITextBuffer removedBuffer in removedBuffers)
+ {
+ if (removedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0)
+ {
+ BaseBuffer baseRemovedBuffer = (BaseBuffer)removedBuffer;
+
+ for (int i = 0; (i < this.eventHooks.Count); ++i)
+ {
+ var hook = this.eventHooks[i];
+ if (hook.SourceBuffer == baseRemovedBuffer)
+ {
+ hook.UnsubscribeFromSourceBuffer();
+ this.eventHooks.RemoveAt(i);
+ break;
+ }
+ }
+ }
+ }
+
+ args = new ProjectionSourceBuffersChangedEventArgs
+ (beforeSnapshot, this.currentProjectionSnapshot,
+ spansToInsert, deletedSpans, position, addedBuffers, removedBuffers, options, editTag);
+ }
+ else
+ {
+ args = new ProjectionSourceSpansChangedEventArgs
+ (beforeSnapshot, this.currentProjectionSnapshot,
+ spansToInsert, deletedSpans, position, options, editTag);
+ }
+ return args;
+ }
+
+ private INormalizedTextChangeCollection ComputeTextChangesByStringDiffing(StringDifferenceOptions differenceOptions, int textPosition, List<SnapshotSpan> deletedSnapSpans, List<SnapshotSpan> insertedSnapSpans)
+ {
+ StringBuilder oldText = new StringBuilder();
+ foreach (SnapshotSpan deletedSnapSpan in deletedSnapSpans)
+ {
+ oldText.Append(deletedSnapSpan.GetText());
+ }
+
+ StringBuilder newText = new StringBuilder();
+ foreach (SnapshotSpan insertedSnapSpan in insertedSnapSpans)
+ {
+ newText.Append(insertedSnapSpan.GetText());
+ }
+
+ List<TextChange> changes = new List<TextChange>();
+ if (oldText.Length > 0 || newText.Length > 0)
+ {
+ changes.Add(TextChange.Create(textPosition, oldText.ToString(), newText.ToString(), this.currentProjectionSnapshot));
+ }
+
+ return NormalizedTextChangeCollection.Create(changes, differenceOptions, this.textDifferencingService);
+ }
+
+ private INormalizedTextChangeCollection ComputeTextChangesBySpanDiffing(int textPosition, List<SnapshotSpan> deletedSnapSpans, List<SnapshotSpan> insertedSnapSpans)
+ {
+ ProjectionSpanDiffer differ = new ProjectionSpanDiffer(this.differenceService, deletedSnapSpans.AsReadOnly(), insertedSnapSpans.AsReadOnly());
+ return new ProjectionSpanToNormalizedChangeConverter(differ, textPosition, this.currentProjectionSnapshot).NormalizedChanges;
+ }
+
+ private void AddSpan(int position, ITrackingSpan sourceSpan)
+ {
+ this.sourceSpans.Insert(position, sourceSpan);
+ this.sourceBufferSet.AddSpan(sourceSpan);
+ }
+
+ private ITrackingSpan RemoveSpan(int position)
+ {
+ ITrackingSpan result = this.sourceSpans[position];
+ this.sourceSpans.RemoveAt(position);
+ this.sourceBufferSet.RemoveSpan(result);
+ return result;
+ }
+
+ public IProjectionSnapshot InsertSpan(int position, ITrackingSpan spanToInsert)
+ {
+ return ReplaceSpans(position, 0, new List<object>(1) { spanToInsert }, EditOptions.None, null);
+ }
+
+ public IProjectionSnapshot InsertSpan(int position, string literalSpanToInsert)
+ {
+ return ReplaceSpans(position, 0, new List<object>(1) { literalSpanToInsert }, EditOptions.None, null);
+ }
+
+ public IProjectionSnapshot InsertSpans(int position, IList<object> spansToInsert)
+ {
+ return ReplaceSpans(position, 0, spansToInsert, EditOptions.None, null);
+ }
+
+ public IProjectionSnapshot DeleteSpans(int position, int spansToDelete)
+ {
+ return ReplaceSpans(position, spansToDelete, new List<object>(0), EditOptions.None, null);
+ }
+
+ public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList<object> spansToInsert, EditOptions options, object editTag)
+ {
+ using (SpanEdit spedit = new SpanEdit(this))
+ {
+ return spedit.ReplaceSpans(position, spansToReplace, spansToInsert, options, editTag);
+ }
+ }
+ #endregion
+
+ #region Edit Creation
+ public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+
+ protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+ #endregion
+
+ #region Event Handling
+ /// <summary>
+ /// Respond to a content change in a source buffer by mapping the change events to the coordinate system of
+ /// this projection buffer.
+ /// </summary>
+ internal override void OnSourceTextChanged(object sender, TextContentChangedEventArgs e)
+ {
+ this.pendingContentChangedEventArgs.Add(e);
+
+ if (!this.editApplicationInProgress)
+ {
+ // We had better be a member of the same group as the buffer that we just heard from
+ Debug.Assert(this.group.MembersContains(e.After.TextBuffer));
+ // Let the buffer group decide when to issue events; this allows us to collect changes from multiple
+ // source buffers into a single snapshot, and (more important) to avoid inconsistencies
+ this.group.ScheduleIndependentEdit(this);
+ }
+ }
+ #endregion
+
+ #region Source Change Interpretation
+
+ public override BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag)
+ {
+ IList<ITextEventRaiser> eventRaisers = InterpretSourceChanges(options, /*this.reiteratedVersionNumber,*/ editTag);
+ Debug.Assert(eventRaisers.Count == 1);
+ eventRaisers[0].RaiseEvent(this, true);
+ return eventRaisers[0];
+ }
+
+ /// <summary>
+ /// TextChanges that are to be accomplished by a change to source spans (because of EdgeExclusive anomalies).
+ /// </summary>
+ private class SpanAdjustment
+ {
+ public TextChange LeadingChange; // Text change induced by span growth/shrinkage on the leading edge
+ public TextChange TrailingChange; // Text change induced by span growth/shrinkage on the trailing edge
+ }
+
+ private IList<ITextEventRaiser> InterpretSourceChanges(EditOptions options, object editTag)
+ {
+ // now that all source edits have been applied, we can interpret the events they raised
+ List<ITextEventRaiser> eventRaisers = new List<ITextEventRaiser>();
+
+ SortedDictionary<int, SpanAdjustment> spanPreAdjustments = new SortedDictionary<int, SpanAdjustment>();
+ SortedDictionary<int, SpanAdjustment> spanPostAdjustments = new SortedDictionary<int, SpanAdjustment>();
+
+ INormalizedTextChangeCollection normalizedChanges = ComputeProjectedChanges(spanPreAdjustments, spanPostAdjustments);
+
+ // Shrink source spans for each span pre-adjustment. Each one generates a new projection version/snapshot.
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("Span Preadjustments");
+ }
+ foreach (KeyValuePair<int, SpanAdjustment> positionAndAdjustment in spanPreAdjustments)
+ {
+ eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, true, editTag));
+ }
+
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("Main Snapshot");
+ }
+ //if (normalizedChanges.Count > 0) // TODO: optimize this snapshot away if Count == 0
+ {
+ ITextSnapshot originSnapshot = this.currentProjectionSnapshot;
+ base.SetCurrentVersionAndSnapshot(normalizedChanges);
+ eventRaisers.Add(new TextContentChangedEventRaiser(originSnapshot, this.currentProjectionSnapshot, options, editTag));
+ }
+
+ // Generate additional transaction(s) that alter EdgeExclusive source spans as needed
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("Span Postadjustments");
+ }
+ foreach (KeyValuePair<int, SpanAdjustment> positionAndAdjustment in spanPostAdjustments)
+ {
+ eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, false, editTag));
+ }
+
+ this.editApplicationInProgress = false;
+ return eventRaisers;
+ }
+
+ private SourceSpansChangedEventRaiser PerformSpanAdjustments(int spanPosition, SpanAdjustment adjustment, bool beforeBaseSnapshot, object editTag)
+ {
+ IProjectionSnapshot originSnapshot = this.currentProjectionSnapshot;
+ List<SnapshotSpan> newSourceSpans = new List<SnapshotSpan>(originSnapshot.GetSourceSpans());
+ this.sourceBufferSet.StartTransaction();
+
+ ITrackingSpan deletedSpan = RemoveSpan(spanPosition);
+ SnapshotSpan sourceOriginSpan = originSnapshot.GetSourceSpans(spanPosition, 1)[0];
+ int start = sourceOriginSpan.Start;
+ int end = sourceOriginSpan.End;
+
+ List<TextChange> changes = new List<TextChange>();
+ if (adjustment.LeadingChange != null)
+ {
+ changes.Add(adjustment.LeadingChange);
+ if (beforeBaseSnapshot)
+ {
+ start += adjustment.LeadingChange.OldLength;
+ }
+ else
+ {
+ start -= adjustment.LeadingChange.NewLength;
+ }
+ }
+ if (adjustment.TrailingChange != null)
+ {
+ changes.Add(adjustment.TrailingChange);
+ if (beforeBaseSnapshot)
+ {
+ end -= adjustment.TrailingChange.OldLength;
+ }
+ else
+ {
+ end += adjustment.TrailingChange.NewLength;
+ }
+ }
+ ITrackingSpan insertedSpan = sourceOriginSpan.Snapshot.CreateTrackingSpan(Span.FromBounds(start, end), deletedSpan.TrackingMode);
+ AddSpan(spanPosition, insertedSpan);
+ newSourceSpans[spanPosition] = new SnapshotSpan(sourceOriginSpan.Snapshot, Span.FromBounds(start, end));
+
+ FrugalList<ITextBuffer> addedBuffers;
+ FrugalList<ITextBuffer> removedBuffers;
+ this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers);
+
+ INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(changes);
+
+ this.currentVersion = this.currentVersion.CreateNext(normalizedChanges, newLength: -1, reiteratedVersionNumber: -1);
+ this.builder = this.ApplyChangesToStringRebuilder(normalizedChanges, this.builder);
+
+ if (beforeBaseSnapshot)
+ {
+ this.currentProjectionSnapshot = TakeStaticSnapshot(newSourceSpans);
+ }
+ else
+ {
+ this.currentSnapshot = TakeSnapshot();
+ }
+
+ ProjectionSourceSpansChangedEventArgs args =
+ new ProjectionSourceSpansChangedEventArgs(originSnapshot, this.currentProjectionSnapshot,
+ new ITrackingSpan[] { insertedSpan }, new ITrackingSpan[] { deletedSpan },
+ spanPosition, EditOptions.None, editTag);
+ return new SourceSpansChangedEventRaiser(args);
+ }
+
+ private void DeleteFromSource(SnapshotSpan deletionSpan)
+ {
+ ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer;
+ ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer);
+ xedit.Delete(deletionSpan);
+ }
+
+ private void ReplaceInSource(SnapshotSpan deletionSpan, string insertionText, int masterChangeOffset)
+ {
+ ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer;
+ ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer);
+ xedit.Replace(deletionSpan, insertionText);
+
+ ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset);
+ // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit
+ // and casts them to ITextEdit before returning them.
+ }
+
+ private void InsertInSource(SnapshotPoint sourceInsertionPoint, string text, int masterChangeOffset)
+ {
+ ITextBuffer sourceBuffer = sourceInsertionPoint.Snapshot.TextBuffer;
+ ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer);
+ xedit.Insert(sourceInsertionPoint.Position, text);
+
+ ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset);
+ // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit
+ // and casts them to ITextEdit before returning them.
+ }
+
+ /// <summary>
+ /// Generate the set of normalized text changes to the projection buffer that are induced by the current set of
+ /// pending source changes.
+ /// </summary>
+ /// <param name="spanPreAdjustments">Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s).</param>
+ /// <param name="spanPostAdjustments">Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s).</param>
+ /// <returns></returns>
+ private INormalizedTextChangeCollection ComputeProjectedChanges(SortedDictionary<int, SpanAdjustment> spanPreAdjustments,
+ SortedDictionary<int, SpanAdjustment> spanPostAdjustments)
+ {
+ DumpPendingContentChangedEventArgs();
+ List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges = PreparePendingChanges();
+ List<TextChange> projectedChanges = new List<TextChange>();
+
+ DumpPendingChanges(pendingSourceChanges);
+
+ // these are the points in ordinary text buffers at which we have performed span adjustments
+ // to account for edge-exclusive anomalies. We track them so that we never repeat the adjustment
+ // for the same change.
+ HashSet<SnapshotPoint> urPoints = new HashSet<SnapshotPoint>();
+
+ // Process the pending changes in the reverse of the order in which we received them. This means
+ // that the 'nearest' buffers are processed first.
+ for (int p = pendingSourceChanges.Count - 1; p >= 0; --p)
+ {
+ var pair = pendingSourceChanges[p];
+ List<TextChange> sourceChanges = pair.Item2;
+ int accumulatedDelta = 0;
+ for (int sc = 0; sc < sourceChanges.Count - 1; ++sc) // skip the sentinel
+ {
+ InterpretSourceBufferChange(pair.Item1, sourceChanges[sc], projectedChanges, urPoints, spanPreAdjustments, spanPostAdjustments, accumulatedDelta);
+ accumulatedDelta += sourceChanges[sc].Delta;
+ }
+ }
+
+ if (this.editApplicationInProgress && (spanPreAdjustments.Count > 0 || spanPostAdjustments.Count > 0))
+ {
+ // We may be generating several distinct versions, so we have to do some position normalization.
+ // This only occurs in edits originating on this buffer, not independent changes to source buffers.
+ CorrectSpanAdjustments(projectedChanges, spanPreAdjustments, spanPostAdjustments);
+ }
+
+ return NormalizedTextChangeCollection.Create(projectedChanges);
+ }
+
+ private List<Tuple<ITextBuffer, List<TextChange>>> PreparePendingChanges()
+ {
+ // Changes to source buffers are interpreted against the state of the projection buffer
+ // at the beginning of the transaction. However, events raised by source buffers have
+ // been normalized so that coordinates of later versions are expressed in terms of the
+ // immediately preceding version, not the version at the beginning of the transaction.
+ // Therefore we denormalize the changes before considering them here. This has effect only
+ // when more than one new version has been created for a particular source buffer.
+
+ List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges = new List<Tuple<ITextBuffer, List<TextChange>>>();
+ foreach (TextContentChangedEventArgs args in this.pendingContentChangedEventArgs)
+ {
+ ITextBuffer sourceBuffer = args.Before.TextBuffer;
+
+ // Get the list of pending changes associated with the source buffer. In the case of an
+ // independent change to the source buffer, this list is always empty. For dependent changes,
+ // the list can be nonempty if there are multiple routes to the source buffer.
+ // In the usual case there will be only one change event for a source buffer so this
+ // list will not be found.
+ List<TextChange> bufferChanges;
+ var pair = pendingSourceChanges.Find((p) => (p.Item1 == sourceBuffer));
+ if (pair == null)
+ {
+ bufferChanges = new List<TextChange>(1) { new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None) };
+ pendingSourceChanges.Add(new Tuple<ITextBuffer, List<TextChange>>(sourceBuffer, bufferChanges));
+ }
+ else
+ {
+ bufferChanges = pair.Item2;
+ }
+
+ NormalizedTextChangeCollection.Denormalize(args.Changes, bufferChanges);
+ }
+ this.pendingContentChangedEventArgs.Clear();
+ return pendingSourceChanges;
+ }
+
+ private static FrugalList<Tuple<TextChange, int>> Load(SortedDictionary<int, SpanAdjustment> adjustments)
+ {
+ FrugalList<Tuple<TextChange, int>> result = new FrugalList<Tuple<TextChange, int>>();
+
+ foreach (SpanAdjustment adjustment in adjustments.Values)
+ {
+ int fudge = 0;
+ if (adjustment.LeadingChange != null)
+ {
+ result.Add(new Tuple<TextChange, int>(adjustment.LeadingChange, 0));
+ fudge = adjustment.LeadingChange.Delta;
+ }
+ if (adjustment.TrailingChange != null)
+ {
+ result.Add(new Tuple<TextChange, int>(adjustment.TrailingChange, fudge));
+ }
+ }
+ return result;
+ }
+
+ private static void CorrectSpanAdjustments(List<TextChange> ordinaryChanges,
+ SortedDictionary<int, SpanAdjustment> spanPreAdjustments,
+ SortedDictionary<int, SpanAdjustment> spanPostAdjustments)
+ {
+ TextChange[] sortedOrdinary = TextUtilities.StableSort(ordinaryChanges, (left, right) => (left.OldPosition - right.OldPosition));
+
+ FrugalList<TextChange> ordinary = new FrugalList<TextChange>(sortedOrdinary);
+ FrugalList<Tuple<TextChange, int>> preAdjustments = Load(spanPreAdjustments);
+ FrugalList<Tuple<TextChange, int>> postAdjustments = Load(spanPostAdjustments);
+
+ int count = ordinary.Count + preAdjustments.Count + postAdjustments.Count;
+
+ TextChange sentinel = new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None);
+ ordinary.Add(sentinel);
+ preAdjustments.Add(new Tuple<TextChange, int>(sentinel, 0));
+ postAdjustments.Add(new Tuple<TextChange, int>(sentinel, 0));
+
+ int ordDelta = 0;
+ int preDelta = 0;
+ int postDelta = 0;
+
+ int ordIndex = 0;
+ int preIndex = 0;
+ int postIndex = 0;
+
+ int ordPos = ordinary[0].OldPosition;
+ int prePos = preAdjustments[0].Item1.OldPosition;
+ int postPos = postAdjustments[0].Item1.OldPosition;
+
+ for (int c = 0; (c < count); ++c)
+ {
+ if (postPos <= prePos && postPos <= ordPos)
+ {
+ // post either comes first or is tied. Advance it one step since its adjustments won't affect any of the other adjustments
+ // (they don't depend on postDelta).
+ postAdjustments[postIndex].Item1.OldPosition += preDelta + ordDelta + postDelta - postAdjustments[postIndex].Item2;
+ postAdjustments[postIndex].Item1.NewPosition = postAdjustments[postIndex].Item1.OldPosition;
+ postDelta += postAdjustments[postIndex].Item1.Delta;
+ postPos = postAdjustments[++postIndex].Item1.OldPosition;
+ }
+ else if (ordPos <= prePos)
+ {
+ // The ordinary change comes before or is tied with pre and comes before post. Advance it one step since its adjustment won't
+ // affect pre's adjustment.
+
+ // postPos comes after ordPos or prePos.
+ // if ordPos is before prePos, then it must also come before postPos.
+ Debug.Assert(ordPos < postPos);
+
+ ordinary[ordIndex].OldPosition += preDelta;
+ ordinary[ordIndex].NewPosition = ordinary[ordIndex].OldPosition;
+ ordDelta += ordinary[ordIndex].Delta;
+ ordPos = ordinary[++ordIndex].OldPosition;
+ }
+ else
+ {
+ Debug.Assert(prePos < ordPos);
+ Debug.Assert(prePos < postPos);
+
+ preAdjustments[preIndex].Item1.OldPosition += preDelta - preAdjustments[preIndex].Item2;
+ preAdjustments[preIndex].Item1.NewPosition = preAdjustments[preIndex].Item1.OldPosition;
+ preDelta += preAdjustments[preIndex].Item1.Delta;
+ prePos = preAdjustments[++preIndex].Item1.OldPosition;
+ }
+ }
+
+ Debug.Assert(ordIndex == ordinary.Count - 1);
+ Debug.Assert(preIndex == preAdjustments.Count - 1);
+ Debug.Assert(postIndex == postAdjustments.Count - 1);
+ }
+
+
+ /// <summary>
+ /// Figure out how a source buffer change affects the projection buffer.
+ /// </summary>
+ /// <param name="changedBuffer">The source buffer.</param>
+ /// <param name="change">The TextChange received from the source buffer.</param>
+ /// <param name="projectedChanges">List of changes to projection buffer to be augmented if <paramref name="change"/>
+ /// applies to the source buffer and is not otherwised covered by a span adjustment.</param>
+ /// <param name="urPoints"></param>
+ /// <param name="spanPreAdjustments">Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s).</param>
+ /// <param name="spanPostAdjustments">Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s).</param>
+ private void InterpretSourceBufferChange(ITextBuffer changedBuffer,
+ ITextChange change,
+ List<TextChange> projectedChanges,
+ HashSet<SnapshotPoint> urPoints,
+ SortedDictionary<int, SpanAdjustment> spanPreAdjustments,
+ SortedDictionary<int, SpanAdjustment> spanPostAdjustments,
+ int accumulatedDelta)
+ {
+ ProjectionSnapshot priorSnapshot = this.currentProjectionSnapshot;
+ int sourceChangePosition = change.NewPosition;
+ Span deletionSpan = new Span(sourceChangePosition, change.OldLength);
+ int insertionCount = change.NewLength;
+ int cumulativeLength = 0;
+
+ int spanPosition = 0;
+
+ ITextSnapshot afterSourceSnapshot = changedBuffer.CurrentSnapshot;
+ // todo: consider whether need to use a more precise snapshot. I don't think so, but give it more thought.
+ // this is used only for mapping to urPoints.
+
+ // This algorithm does a linear search of source spans in forward order.
+
+ foreach (ITrackingSpan sourceSpan in this.sourceSpans)
+ {
+ SnapshotSpan priorRawSpan = priorSnapshot.GetSourceSpan(spanPosition);
+ // Note: if we switch back to not generating a new snapshot of the projection buffer on every source
+ // buffer change, then here we have to be careful to map the priorRawSpan to the current snapshot of the source buffer,
+ // since it might be coming from an old snapshot (see e.g. Edit00 unit test)
+
+ if (sourceSpan.TextBuffer == changedBuffer)
+ {
+ SpanTrackingMode mode = sourceSpan.TrackingMode;
+ // is there an easy way to handle custom spans here?
+ Span? deletedHere = deletionSpan.Overlap(priorRawSpan);
+ // n.b.: Null span does not overlap with anything
+ if (deletedHere.HasValue && deletedHere.Value.Length > 0)
+ {
+ // part or all of the source span was deleted by the change
+
+ // compute the position at which the change takes place in the projection buffer
+ // with respect to its current snapshot
+ int projectedPosition = cumulativeLength + deletedHere.Value.Start - priorRawSpan.Start;
+
+ Debug.Assert(projectedPosition >= 0 && projectedPosition <= priorSnapshot.Length);
+
+ StringRebuilder deletedText = TextChange.ChangeOldSubText(change, Math.Max(priorRawSpan.Start.Position - deletionSpan.Start, 0), deletedHere.Value.Length);
+ StringRebuilder insertedText = StringRebuilder.Empty;
+
+ SnapshotSpan adjustedPriorRawSpan = new SnapshotSpan(priorRawSpan.Snapshot, priorRawSpan.Start, priorRawSpan.Length - deletedText.Length);
+
+ if (sourceSpan.TrackingMode != SpanTrackingMode.EdgeInclusive && sourceSpan.TrackingMode != SpanTrackingMode.Custom && this.editInProgress)
+ {
+ // the tricky cases. If the deletion touches the edge of the source span, we first explicitly
+ // shrink the span to effect the deletion. If the change is later undone, the source span
+ // will be grown explicitly to encompass the restored text (otherwise we would lose it since
+ // the source span is EdgeExclusive and won't grow on its own).
+ if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgeNegative) && (deletedHere.Value.Start == priorRawSpan.Start))
+ {
+ // A prefix of the source span (or the whole thing) is to be shrunk to effect the deletion.
+ SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition);
+ // Create the text change that will be induced by the span adjustment
+ Debug.Assert(adjust.LeadingChange == null); // there can only be one leading change for a particular span
+ adjust.LeadingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot);
+ Debug.Assert(adjust.LeadingChange.OldEnd <= priorSnapshot.Length);
+ deletedText = StringRebuilder.Empty;
+ }
+ else if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgePositive) && (deletedHere.Value.End == priorRawSpan.End))
+ {
+ // A suffix of the source span is to be shrunk to effect the deletion.
+ SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition);
+ // Create the text change that will be induced by the span adjustment
+ Debug.Assert(adjust.TrailingChange == null);
+ adjust.TrailingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot);
+ Debug.Assert(adjust.TrailingChange.OldEnd <= priorSnapshot.Length);
+ deletedText = StringRebuilder.Empty;
+ }
+ }
+
+ if (change.NewLength > 0) // change includes an insertion
+ {
+ insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, adjustedPriorRawSpan, deletionSpan,
+ sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta);
+ if (insertedText.Length > 0)
+ {
+ // replacement string is inserted here. There can be more than one insertion
+ // per change if custom tracking spans are involved.
+ insertionCount = change.NewLength - insertedText.Length;
+ }
+ }
+
+ if (deletedText.Length > 0 || insertedText.Length > 0)
+ {
+ TextChange interpretedChange = TextChange.Create(projectedPosition, deletedText, insertedText, this.currentProjectionSnapshot);
+ Debug.Assert(interpretedChange.OldEnd <= priorSnapshot.Length);
+ projectedChanges.Add(interpretedChange);
+ }
+ }
+ else if (insertionCount > 0)
+ {
+ int projectedPosition = cumulativeLength + Math.Max(sourceChangePosition - priorRawSpan.Start, 0);
+ // if the insertion is part of a replacement and the source span in question is edge inclusive, a sourceChangePosition to the
+ // left of the current source span may actually end up being interesting, in which case it would be at the beginning of the span.
+ // If those conditions don't obtain, InsertionLiesInSpan will return false and nobody will be the wiser.
+ int hack = spanPostAdjustments == null ? 0 : spanPostAdjustments.Count;
+ StringRebuilder insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, priorRawSpan, deletionSpan,
+ sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta);
+ if (insertedText.Length > 0)
+ {
+ // a pure insertion into the source span
+
+ TextChange interpretedChange = TextChange.Create(projectedPosition, string.Empty, insertedText, this.currentProjectionSnapshot);
+ projectedChanges.Add(interpretedChange);
+ }
+ if (spanPostAdjustments != null && spanPostAdjustments.Count != hack) // ur points should have eliminated the need for the hack
+ {
+ insertionCount = 0;
+ }
+ }
+ }
+ cumulativeLength += priorRawSpan.Length;
+ spanPosition++;
+ }
+ }
+
+ private StringRebuilder InsertionLiesInSpan(ITextSnapshot afterSourceSnapshot,
+ int projectedPosition,
+ int spanPosition,
+ SnapshotSpan rawSpan,
+ Span deletionSpan,
+ int sourcePosition,
+ SpanTrackingMode mode,
+ ITextChange incomingChange,
+ HashSet<SnapshotPoint> urPoints,
+ SortedDictionary<int, SpanAdjustment> spanAdjustments,
+ int accumulatedDelta)
+ {
+ int renormalizedSourcePosition = sourcePosition + accumulatedDelta;
+ if (mode == SpanTrackingMode.Custom)
+ {
+ return InsertionLiesInCustomSpan(afterSourceSnapshot, spanPosition, incomingChange, urPoints, accumulatedDelta);
+ }
+
+ bool contains = rawSpan.Contains(sourcePosition);
+ if (mode == SpanTrackingMode.EdgeInclusive)
+ {
+ if ((this.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) != 0)
+ {
+ return InsertionLiesInPermissiveInclusiveSpan
+ (afterSourceSnapshot, rawSpan, deletionSpan, sourcePosition, renormalizedSourcePosition, incomingChange, urPoints);
+ }
+ else
+ {
+ return contains || sourcePosition == rawSpan.End ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty;
+ }
+ }
+ else
+ {
+ if (!this.editInProgress)
+ {
+ // Edit originated in the source buffer; we don't do any implicit growing
+ // of spans
+ bool included;
+ if (mode == SpanTrackingMode.EdgeNegative)
+ {
+ included = contains;
+ }
+ else if (mode == SpanTrackingMode.EdgePositive)
+ {
+ included = (sourcePosition != rawSpan.Start) && (contains || sourcePosition == rawSpan.End);
+ }
+ else
+ {
+ included = contains && sourcePosition != rawSpan.Start;
+ }
+ return included ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty;
+ }
+ else
+ {
+ if (sourcePosition == rawSpan.Start && (mode != SpanTrackingMode.EdgeNegative))
+ {
+ SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition),
+ (buffer) => (buffer is TextBuffer),
+ PositionAffinity.Successor);
+ Debug.Assert(urPoint.HasValue);
+ if (urPoints.Add(urPoint.Value))
+ {
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("UR-Point [exclusive:start]" + urPoint.Value.ToString());
+ }
+ // Insertion at exclusive left edge of source span: we need to grow the source span on the left
+ SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition);
+ // Create the text change that will be induced by the span adjustment
+ Debug.Assert(adjust.LeadingChange == null);
+ adjust.LeadingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot);
+ }
+ return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one
+ }
+ else if (sourcePosition == rawSpan.End && (mode != SpanTrackingMode.EdgePositive))
+ {
+ SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition),
+ (buffer) => (buffer is TextBuffer),
+ PositionAffinity.Predecessor);
+ Debug.Assert(urPoint.HasValue);
+ if (urPoints.Add(urPoint.Value))
+ {
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("UR-Point [exclusive:end]" + urPoint.Value.ToString());
+ }
+ // Insertion at exclusive right edge of source span: we need to grow the source span on the right
+ SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition);
+ // Create the text change that will be induced by the span adjustment
+ Debug.Assert(adjust.TrailingChange == null);
+ adjust.TrailingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot);
+ }
+ return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one
+ }
+ return (contains || (mode == SpanTrackingMode.EdgePositive && sourcePosition == rawSpan.End)) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty;
+ }
+ }
+ }
+
+ private StringRebuilder InsertionLiesInCustomSpan(ITextSnapshot afterSourceSnapshot,
+ int spanPosition,
+ ITextChange incomingChange,
+ HashSet<SnapshotPoint> urPoints,
+ int accumulatedDelta)
+ {
+ // just evaluate the new span and see if it overlaps the insertion.
+ ITrackingSpan sourceTrackingSpan = this.sourceSpans[spanPosition];
+ SnapshotSpan afterSpan = sourceTrackingSpan.GetSpan(afterSourceSnapshot);
+
+ Span newSpan = new Span(incomingChange.NewPosition + accumulatedDelta, incomingChange.NewLength);
+ Span? over = newSpan.Overlap(afterSpan);
+ return over.HasValue ? BufferFactoryService.StringRebuilderFromSnapshotAndSpan(afterSourceSnapshot, over.Value) : StringRebuilder.Empty;
+
+
+ //if (futureSpan.Contains(renormalizedSourcePosition))
+ //{
+ // if (BufferGroup.Tracing)
+ // {
+ // Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ // "Custom renormPosition {0} priorSpan [{1}..{2})", renormalizedSourcePosition, priorSpan.Start.Position + accumulatedDelta, priorSpan.End.Position + accumulatedDelta));
+ // }
+ // if (renormalizedSourcePosition == (priorSpan.Start.Position + accumulatedDelta) || renormalizedSourcePosition == (priorSpan.End.Position + accumulatedDelta))
+ // {
+ // SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition),
+ // (buffer) => (buffer is TextBuffer),
+ // renormalizedSourcePosition == priorSpan.Start.Position + accumulatedDelta
+ // ? PositionAffinity.Successor
+ // : PositionAffinity.Predecessor);
+ // Debug.Assert(urPoint.HasValue);
+ // bool added = urPoints.Add(urPoint.Value);
+ // Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point
+ // if (BufferGroup.Tracing)
+ // {
+ // Debug.WriteLine("UR-Point [custom]" + urPoint.Value.ToString());
+ // }
+ // }
+ // return true;
+ //}
+ //else
+ //{
+ // return false;
+ //}
+ }
+
+ private static StringRebuilder InsertionLiesInPermissiveInclusiveSpan(ITextSnapshot afterSourceSnapshot,
+ SnapshotSpan rawSpan,
+ Span deletionSpan,
+ int sourcePosition,
+ int renormalizedSourcePosition,
+ ITextChange incomingChange,
+ HashSet<SnapshotPoint> urPoints)
+ {
+ bool leadingInclusiveGrowth = sourcePosition < rawSpan.Start && deletionSpan.End >= rawSpan.Start;
+ if (sourcePosition == rawSpan.Start || leadingInclusiveGrowth)
+ {
+ SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition),
+ (buffer) => (buffer is TextBuffer),
+ PositionAffinity.Successor);
+ Debug.Assert(urPoint.HasValue);
+ bool added = urPoints.Add(urPoint.Value);
+ Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("UR-Point [inclusive:start]" + urPoint.Value.ToString());
+ }
+ return TextChange.NewStringRebuilder(incomingChange);
+ }
+ else if (sourcePosition == rawSpan.End)
+ {
+ SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition),
+ (buffer) => (buffer is TextBuffer),
+ PositionAffinity.Predecessor);
+ Debug.Assert(urPoint.HasValue);
+ bool added = urPoints.Add(urPoint.Value);
+ Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine("UR-Point [inclusive:end]" + urPoint.Value.ToString());
+ }
+ return TextChange.NewStringRebuilder(incomingChange);
+ }
+ else
+ {
+ return rawSpan.Contains(sourcePosition) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty;
+ }
+ }
+
+ /// <summary>
+ /// Fetch <see cref="SpanAdjustment"/> for this span if it already exists, or create a new one.
+ /// </summary>
+ private static SpanAdjustment GetAdjustment(SortedDictionary<int, SpanAdjustment> spanAdjustments, int spanPosition)
+ {
+ SpanAdjustment adjust;
+ if (!spanAdjustments.TryGetValue(spanPosition, out adjust))
+ {
+ adjust = new SpanAdjustment();
+ spanAdjustments.Add(spanPosition, adjust);
+ }
+ return adjust;
+ }
+ #endregion
+
+ #region Change Application
+ /// <summary>
+ /// Given the set of changes to apply to this buffer, compute the set of changes to apply to its
+ /// source buffers. These edit objects are managed by the buffer group, which will decide when to
+ /// apply them.
+ /// </summary>
+ private void ComputeSourceEdits(FrugalList<TextChange> changes)
+ {
+ foreach (TextChange change in changes)
+ {
+ if (change.OldLength > 0 && change.NewLength == 0)
+ {
+ // the change is a deletion
+ IList<SnapshotSpan> sourceDeletionSpans = this.currentProjectionSnapshot.MapToSourceSnapshots(new Span(change.NewPosition, change.OldLength));
+ foreach (SnapshotSpan sourceDeletionSpan in sourceDeletionSpans)
+ {
+ DeleteFromSource(sourceDeletionSpan);
+ }
+ }
+
+ else if (change.OldLength > 0 && change.NewLength > 0)
+ {
+ // the change is a replacement
+ ReadOnlyCollection<SnapshotSpan> allSourceReplacementSpans =
+ this.currentProjectionSnapshot.MapReplacementSpanToSourceSnapshots
+ (new Span(change.OldPosition, change.OldLength), (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null);
+
+ //Filter out replacement spans that are read-only (since we couldn't edit them in any case).
+ FrugalList<SnapshotSpan> sourceReplacementSpans = new FrugalList<SnapshotSpan>();
+ foreach (var s in allSourceReplacementSpans)
+ {
+ if (!s.Snapshot.TextBuffer.IsReadOnly(s.Span, true))
+ sourceReplacementSpans.Add(s);
+ }
+
+ Debug.Assert(sourceReplacementSpans.Count > 0); // if replacement is on read-only buffers, the read only check will have already caught it
+
+ if (sourceReplacementSpans.Count == 1)
+ {
+ ReplaceInSource(sourceReplacementSpans[0], change.NewText, 0 + change.MasterChangeOffset);
+ }
+ else
+ {
+ // the replacement hits the boundary of source spans
+ int[] insertionSizes = new int[sourceReplacementSpans.Count];
+
+ if (this.resolver != null)
+ {
+ SnapshotSpan projectionReplacementSpan = new SnapshotSpan(this.currentProjectionSnapshot, change.OldPosition, change.OldLength);
+ this.resolver.FillInReplacementSizes(projectionReplacementSpan, new ReadOnlyCollection<SnapshotSpan>(sourceReplacementSpans), change.NewText, insertionSizes);
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "## Seam Replacement @:{0}", projectionReplacementSpan));
+ for (int s = 0; s < sourceReplacementSpans.Count; ++s)
+ {
+ Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "## {0,4}: {1}", insertionSizes[s], sourceReplacementSpans[s]));
+ }
+ Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "## Replacement Text:'{0}'", TextUtilities.Escape(change.NewText)));
+ }
+ }
+ insertionSizes[insertionSizes.Length - 1] = int.MaxValue;
+
+ int pos = 0;
+ for (int i = 0; i < insertionSizes.Length; ++i)
+ {
+ // contend with any old garbage that the client passed back.
+ int insertionSize = Math.Min(insertionSizes[i], change.NewLength - pos);
+ if (insertionSize > 0)
+ {
+ ReplaceInSource(sourceReplacementSpans[i], TextChange.ChangeNewSubstring(change, pos, insertionSize), pos + change.MasterChangeOffset);
+ pos += insertionSize;
+ }
+ else if (sourceReplacementSpans[i].Length > 0)
+ {
+ DeleteFromSource(sourceReplacementSpans[i]);
+ }
+ }
+ }
+ }
+ else
+ {
+ Debug.Assert(change.OldLength == 0 && change.NewLength > 0);
+ // the change is an insertion
+ ReadOnlyCollection<SnapshotPoint> allSourceInsertionPoints =
+ this.currentProjectionSnapshot.MapInsertionPointToSourceSnapshots
+ (change.NewPosition, (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null);
+
+ Debug.Assert(allSourceInsertionPoints.Count > 0); // if insertion point is between two literal spans, the read only check will have already caught it
+
+ //Filter out replacement spans that are read-only (since we couldn't edit them in any case).
+ FrugalList<SnapshotPoint> sourceInsertionPoints = new FrugalList<SnapshotPoint>();
+ foreach (var p in allSourceInsertionPoints)
+ {
+ if (!p.Snapshot.TextBuffer.IsReadOnly(p.Position, true))
+ sourceInsertionPoints.Add(p);
+ }
+
+ Debug.Assert(sourceInsertionPoints.Count > 0); // if insertion point is between only read-only buffers, the read only check will have already caught it
+
+ if (sourceInsertionPoints.Count == 1)
+ {
+ // the insertion point is unambiguous
+ InsertInSource(sourceInsertionPoints[0], change.NewText, 0 + change.MasterChangeOffset);
+ }
+ else
+ {
+ // the insertion is at the boundary of source spans
+ int[] insertionSizes = new int[sourceInsertionPoints.Count];
+
+ if (this.resolver != null)
+ {
+ this.resolver.FillInInsertionSizes(new SnapshotPoint(this.currentProjectionSnapshot, change.NewPosition),
+ new ReadOnlyCollection<SnapshotPoint>(sourceInsertionPoints), change.NewText, insertionSizes);
+ }
+
+ // if resolver was not provided, we just use zeros for the insertion sizes, which will push the entire insertion
+ // into the last slot.
+ insertionSizes[insertionSizes.Length - 1] = int.MaxValue;
+
+ int pos = 0;
+ for (int i = 0; i < insertionSizes.Length; ++i)
+ {
+ // contend with any old garbage that the client passed back.
+ int size = Math.Min(insertionSizes[i], change.NewLength - pos);
+ if (size > 0)
+ {
+ InsertInSource(sourceInsertionPoints[i], change._newText.GetText(new Span(pos, size)), pos + change.MasterChangeOffset);
+ pos += size;
+ if (pos == change.NewLength)
+ {
+ break; // inserted text is used up, whether we've visited all of the insertionSizes or not
+ }
+ }
+ }
+ }
+ }
+ }
+ // defer interpretation of events that will be raised by source buffers as we make these edits
+ this.editApplicationInProgress = true;
+ }
+
+ #endregion
+
+ #region Snapshots
+ protected override BaseSnapshot TakeSnapshot()
+ {
+ List<SnapshotSpan> newSourceSpans = new List<SnapshotSpan>(this.sourceSpans.Count);
+ foreach (ITrackingSpan sourceSpan in this.sourceSpans)
+ {
+ // since we are on the main thread, we can safely just look at current snapshots
+ newSourceSpans.Add(sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot));
+ }
+ this.currentProjectionSnapshot = MakeSnapshot(newSourceSpans);
+ return this.currentProjectionSnapshot;
+ }
+
+ private ProjectionSnapshot TakeStaticSnapshot(List<SnapshotSpan> newSourceSpans)
+ {
+ // this form of snapshot uses the same source snapshots as current snapshot rather than current snapshots
+ return MakeSnapshot(newSourceSpans);
+ }
+
+ private ProjectionSnapshot MakeSnapshot(List<SnapshotSpan> newSourceSpans)
+ {
+ return new ProjectionSnapshot(this, this.currentVersion, this.builder, newSourceSpans);
+ }
+
+ protected override StringRebuilder GetDoppelgangerBuilder()
+ {
+ ITextBuffer doppelBottom;
+ if (Properties.TryGetProperty<ITextBuffer>("IdentityMapping", out doppelBottom))
+ {
+ var snapshot = doppelBottom.CurrentSnapshot;
+ return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(snapshot, new Span(0, snapshot.Length));
+ }
+
+ if (this.sourceSpans.Count == 1)
+ {
+ var sourceSpan = this.sourceSpans[0];
+ var source = sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot);
+ if ((source.Length == this.currentVersion.Length) && (source.Snapshot is BaseSnapshot))
+ {
+ // We're mapped to a single span that is equal to the entire contents the source buffer.
+ return BufferFactoryService.StringRebuilderFromSnapshotSpan(source);
+ }
+ }
+
+ return null;
+ }
+
+ public override IProjectionSnapshot CurrentSnapshot
+ {
+ get { return this.currentProjectionSnapshot; }
+ }
+
+ protected override BaseProjectionSnapshot CurrentBaseSnapshot
+ {
+ get { return this.currentProjectionSnapshot; }
+ }
+ #endregion
+
+ #region Events
+ internal event EventHandler<ProjectionSourceBuffersChangedEventArgs> SourceBuffersChangedImmediate;
+ internal event EventHandler<ProjectionSourceSpansChangedEventArgs> SourceSpansChangedImmediate;
+
+ public event EventHandler<ProjectionSourceBuffersChangedEventArgs> SourceBuffersChanged;
+ public event EventHandler<ProjectionSourceSpansChangedEventArgs> SourceSpansChanged;
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs
new file mode 100644
index 0000000..5414af1
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs
@@ -0,0 +1,757 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Diagnostics;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Linq;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ using Strings = Microsoft.VisualStudio.Text.Implementation.Strings;
+
+ internal partial class ProjectionSnapshot : BaseProjectionSnapshot, IProjectionSnapshot
+ {
+ #region Private members
+ private readonly ProjectionBuffer projectionBuffer;
+ private ReadOnlyCollection<SnapshotSpan> sourceSpans;
+ private ReadOnlyCollection<ITextSnapshot> sourceSnapshots;
+
+
+ /// <summary>
+ /// Denotes a source span and its character position in the projection snapshot.
+ /// </summary>
+ struct InvertedSource
+ {
+ public readonly Span sourceSpan;
+ public readonly int projectedPosition;
+ public InvertedSource(Span sourceSpan, int projectedPosition)
+ {
+ this.sourceSpan = sourceSpan;
+ this.projectedPosition = projectedPosition;
+ }
+ }
+
+ /// <summary>
+ /// For each source snapshot, a list of the source spans from that snapshot, plus their positions in the
+ /// projection snapshot, ordered by their positions in the source snapshot. This enables fast mapping from
+ /// source snapshot positions/spans to the equivalent in this projection snapshot. For projection buffers
+ /// with many source spans, the (time) overhead of creating this structure is about 10% of the snapshot cost.
+ /// </summary>
+ private Dictionary<ITextSnapshot, List<InvertedSource>> sourceSnapshotMap;
+
+ private int[] cumulativeLineBreakCounts;
+ private int[] cumulativeLengths;
+ #endregion
+
+ #region Construction
+ public ProjectionSnapshot(ProjectionBuffer projectionBuffer, ITextVersion2 version, StringRebuilder content, IList<SnapshotSpan> sourceSpans)
+ : base(version, content)
+ {
+ this.projectionBuffer = projectionBuffer;
+ this.sourceSpans = new ReadOnlyCollection<SnapshotSpan>(sourceSpans);
+
+ this.cumulativeLengths = new int[sourceSpans.Count + 1];
+ this.cumulativeLineBreakCounts = new int[sourceSpans.Count + 1];
+
+ this.sourceSnapshotMap = new Dictionary<ITextSnapshot, List<InvertedSource>>();
+ for (int s = 0; s < sourceSpans.Count; ++s)
+ {
+ SnapshotSpan sourceSpan = sourceSpans[s];
+ this.totalLength += sourceSpan.Length;
+
+ this.cumulativeLengths[s + 1] = this.cumulativeLengths[s] + sourceSpan.Length;
+
+ // Most source spans won't change when generating a new projection snapshot,
+ // which means we should be able to reuse the line break count from the previous
+ // projection snapshot.
+
+ int lineBreakCount = sourceSpan.Snapshot.GetLineNumberFromPosition(sourceSpan.End) - sourceSpan.Snapshot.GetLineNumberFromPosition(sourceSpan.Start);
+
+ // todo: incorrect when span ends with \r and following begins with \n
+ this.totalLineCount += lineBreakCount;
+
+ this.cumulativeLineBreakCounts[s + 1] = this.cumulativeLineBreakCounts[s] + lineBreakCount;
+
+ ITextSnapshot snapshot = sourceSpan.Snapshot;
+ List<InvertedSource> invertedSources;
+ if (!this.sourceSnapshotMap.TryGetValue(snapshot, out invertedSources))
+ {
+ invertedSources = new List<InvertedSource>();
+ this.sourceSnapshotMap.Add(snapshot, invertedSources);
+ }
+ invertedSources.Add(new InvertedSource(sourceSpan.Span, this.cumulativeLengths[s]));
+ }
+
+ // The SourceSnapshots property is heavily used, so calculate it once
+ this.sourceSnapshots = new ReadOnlyCollection<ITextSnapshot>(new List<ITextSnapshot>(this.sourceSnapshotMap.Keys));
+
+ // sort the per-buffer source span lists by position in source snapshot
+ foreach (var v in this.sourceSnapshotMap.Values)
+ {
+ // sort by starting position. Spans can't overlap, but we do need null spans at a particular position
+ // to precede non-null spans at that position, so if starting positions are equal, compare the ends.
+ v.Sort((left, right) => (left.sourceSpan.Start == right.sourceSpan.Start
+ ? left.sourceSpan.End - right.sourceSpan.End
+ : left.sourceSpan.Start - right.sourceSpan.Start));
+ }
+
+ if (BufferGroup.Tracing)
+ {
+ Debug.WriteLine(LocalToString());
+ }
+ if (this.totalLength != version.Length)
+ {
+ Debug.Fail(string.Format(System.Globalization.CultureInfo.CurrentCulture,
+ "Projection Snapshot Inconsistency. Sum of spans: {0}, Previous + delta: {1}", this.totalLength, version.Length));
+ throw new InvalidOperationException(Strings.InvalidLengthCalculation);
+ }
+ OverlapCheck();
+ }
+
+ private void OverlapCheck()
+ {
+ // try to diagnose problem with overlapping spans
+ Dictionary<ITextSnapshot, List<Span>> groundSourceSpansMap = new Dictionary<ITextSnapshot, List<Span>>();
+ MapDownToGround(this.sourceSpans, groundSourceSpansMap);
+
+ foreach (KeyValuePair<ITextSnapshot, List<Span>> kvp in groundSourceSpansMap)
+ {
+ int sum = 0;
+ foreach (Span s in kvp.Value)
+ {
+ sum += s.Length;
+ }
+ NormalizedSpanCollection norma = new NormalizedSpanCollection(kvp.Value);
+ int normaSum = 0;
+ foreach (Span s in norma)
+ {
+ normaSum += s.Length;
+ }
+ Debug.Assert(sum == normaSum);
+ if (sum != normaSum)
+ {
+ throw new InvalidOperationException(Strings.OverlappingSourceSpans);
+ }
+ }
+ }
+
+ private static void MapDownToGround(IList<SnapshotSpan> spans, Dictionary<ITextSnapshot, List<Span>> groundSourceSpansMap)
+ {
+ foreach (SnapshotSpan span in spans)
+ {
+ IProjectionSnapshot projSnap = span.Snapshot as IProjectionSnapshot;
+ if (projSnap == null)
+ {
+ List<Span> groundSpans;
+ if (!groundSourceSpansMap.TryGetValue(span.Snapshot, out groundSpans))
+ {
+ groundSpans = new List<Span>();
+ groundSourceSpansMap.Add(span.Snapshot, groundSpans);
+ }
+ groundSpans.Add(span);
+ }
+ else
+ {
+ MapDownToGround(projSnap.MapToSourceSnapshots(span), groundSourceSpansMap);
+ }
+ }
+ }
+ #endregion
+
+ #region Buffers and Spans
+ public override IProjectionBufferBase TextBuffer
+ {
+ get { return this.projectionBuffer; }
+ }
+
+ protected override ITextBuffer TextBufferHelper
+ {
+ get { return this.projectionBuffer; }
+ }
+
+ public override int SpanCount
+ {
+ get { return this.sourceSpans.Count; }
+ }
+
+ public override ReadOnlyCollection<ITextSnapshot> SourceSnapshots
+ {
+ get { return this.sourceSnapshots; }
+ }
+
+ public override ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys)
+ {
+ if (snappy.TextBuffer == textBuffer)
+ {
+ return snappy;
+ }
+ }
+ return null;
+ }
+
+ public override ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+ foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys)
+ {
+ if (snappy.TextBuffer == textBuffer)
+ {
+ return snappy;
+ }
+ IProjectionSnapshot2 projSnappy = snappy as IProjectionSnapshot2;
+ if (projSnappy is IProjectionSnapshot2)
+ {
+ ITextSnapshot maybe = projSnappy.GetMatchingSnapshotInClosure(textBuffer);
+ if (maybe != null)
+ {
+ return maybe;
+ }
+ }
+ }
+ return null;
+ }
+
+ public override ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys)
+ {
+ if (match(snappy.TextBuffer))
+ {
+ return snappy;
+ }
+ IProjectionSnapshot2 projSnappy = snappy as IProjectionSnapshot2;
+ if (projSnappy is IProjectionSnapshot2)
+ {
+ ITextSnapshot maybe = projSnappy.GetMatchingSnapshotInClosure(match);
+ if (maybe != null)
+ {
+ return maybe;
+ }
+ }
+ }
+ return null;
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans()
+ {
+ return this.sourceSpans;
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count)
+ {
+ if (startSpanIndex < 0 || startSpanIndex > this.SpanCount)
+ {
+ throw new ArgumentOutOfRangeException("startSpanIndex");
+ }
+ if (count < 0 || startSpanIndex + count > this.SpanCount)
+ {
+ throw new ArgumentOutOfRangeException("count");
+ }
+
+ // better using iterator or explicit successor func eventually
+ List<SnapshotSpan> resultSpans = new List<SnapshotSpan>(count);
+ for (int i = 0; i < count; ++i)
+ {
+ resultSpans.Add(this.sourceSpans[startSpanIndex + i]);
+ }
+ return new ReadOnlyCollection<SnapshotSpan>(resultSpans);
+ }
+
+ internal SnapshotSpan GetSourceSpan(int position)
+ {
+ return this.sourceSpans[position];
+ }
+ #endregion
+
+ #region Mapping
+ public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span)
+ {
+ if (span.End > this.Length)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+
+ FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>();
+
+ if (span.Length == 0)
+ {
+ // First check for a degenerate snapshot having no source spans
+ if (span.Start == 0 && this.sourceSpans.Count == 0)
+ {
+ return new ReadOnlyCollection<SnapshotSpan>(mappedSpans);
+ }
+
+ // Zero-length spans are special in that they may map to more than one zero-length source span.
+ // Defer to the point mapping implementation and then convert back to spans.
+ ReadOnlyCollection<SnapshotPoint> points = MapInsertionPointToSourceSnapshots(span.Start, null);
+ for (int p = 0; p < points.Count; ++p)
+ {
+ SnapshotPoint point = points[p];
+ SnapshotSpan mappedSpan = new SnapshotSpan(point.Snapshot, point.Position, 0);
+ // avoid duplicates, caused by mapping the null span on a seam between source spans
+ // that come from the same source buffer and are adjacent in that source buffer
+ // Example: source spans are [0..10) and [10..20) from same source buffer, and we
+ // are requested to map the span at the seam, corresponding to [10..10).
+ if (mappedSpans.Count == 0 || mappedSpan != mappedSpans[mappedSpans.Count - 1])
+ {
+ mappedSpans.Add(mappedSpan);
+ }
+ }
+ }
+ else
+ {
+ int rover = FindHighestSpanIndexOfPosition(span.Start);
+ // sourceSpans[rover] contains span.Start
+
+ SnapshotSpan sourceSpan = this.sourceSpans[rover];
+ SnapshotPoint mappedStart = sourceSpan.Start + (span.Start - this.cumulativeLengths[rover]);
+ int mappedLength = mappedStart.Position + span.Length < sourceSpan.End ? span.Length : sourceSpan.End.Position - mappedStart;
+ mappedSpans.Add(new SnapshotSpan(mappedStart, mappedLength));
+
+ // walk forward until we cover the entire span
+ while (mappedLength < span.Length)
+ {
+ sourceSpan = this.sourceSpans[++rover];
+ if (span.End >= this.cumulativeLengths[rover + 1])
+ {
+ mappedLength += sourceSpan.Length;
+ mappedSpans.Add(sourceSpan);
+ }
+ else
+ {
+ mappedLength += span.End - this.cumulativeLengths[rover];
+ mappedSpans.Add(new SnapshotSpan(sourceSpan.Snapshot, new Span(sourceSpan.Start, span.End - this.cumulativeLengths[rover])));
+ }
+ }
+ }
+
+ return new ReadOnlyCollection<SnapshotSpan>(mappedSpans);
+ }
+
+ public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span)
+ {
+ return MapToSourceSnapshotsForRead(span);
+ }
+
+ internal override ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer)
+ {
+ // This is exactly like ordinary span mapping, except that empty source spans at the ends of the replacementSpan are included.
+
+ FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>();
+ int rover = FindLowestSpanIndexOfPosition(replacementSpan.Start);
+ int roverHi = FindHighestSpanIndexOfPosition(replacementSpan.End);
+ // sourceSpans[rover] contains span.Start
+
+ SnapshotSpan sourceSpan = this.sourceSpans[rover];
+ {
+ SnapshotPoint mappedStart = sourceSpan.Start + (replacementSpan.Start - this.cumulativeLengths[rover]);
+ int mappedLength = mappedStart.Position + replacementSpan.Length < sourceSpan.End ? replacementSpan.Length : sourceSpan.End.Position - mappedStart;
+ SnapshotSpan mappedSpan = new SnapshotSpan(mappedStart, mappedLength);
+ if (mappedSpan.Length > 0 || mappedSpan.Snapshot.TextBuffer != excludedBuffer)
+ {
+ mappedSpans.Add(new SnapshotSpan(mappedStart, mappedLength));
+ }
+ }
+
+ // walk forward until we cover the entire span
+ while (rover < roverHi)
+ {
+ rover++;
+ sourceSpan = this.sourceSpans[rover];
+ SnapshotSpan mappedSpan = replacementSpan.End >= this.cumulativeLengths[rover + 1]
+ ? sourceSpan
+ : new SnapshotSpan(sourceSpan.Snapshot, new Span(sourceSpan.Start, replacementSpan.End - this.cumulativeLengths[rover]));
+ if (mappedSpan.Length > 0 || mappedSpan.Snapshot.TextBuffer != excludedBuffer)
+ {
+ mappedSpans.Add(mappedSpan);
+ }
+ }
+
+ Debug.Assert(replacementSpan.Length == mappedSpans.Sum((SnapshotSpan s) => s.Length), "Inconsistency in MapReplacementSpanToSourceSnapshots");
+
+ return new ReadOnlyCollection<SnapshotSpan>(mappedSpans);
+ }
+
+ public override ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan sourceSpan)
+ {
+ List<InvertedSource> orderedSources;
+ if (!this.sourceSnapshotMap.TryGetValue(sourceSpan.Snapshot, out orderedSources))
+ {
+ throw new ArgumentException("The span does not belong to a source snapshot of the projection snapshot");
+ }
+
+ Span spanToMap = sourceSpan.Span;
+
+ // binary search for source span containing spanToMap.Start
+ int lo = 0;
+ int hi = orderedSources.Count - 1;
+ int mid = 0;
+ while (lo <= hi)
+ {
+ mid = (lo + hi) / 2;
+
+ if (spanToMap.Start < orderedSources[mid].sourceSpan.Start)
+ {
+ hi = mid - 1;
+ }
+ else if (spanToMap.Start > orderedSources[mid].sourceSpan.End)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ break;
+ // orderedSources[mid].sourceSpan contains (or abuts at the end) sourceSpan.Start
+ }
+ }
+
+ FrugalList<Span> result = new FrugalList<Span>();
+
+ if (spanToMap.Start > orderedSources[mid].sourceSpan.End)
+ {
+ Debug.Assert(hi < lo, "Projection source span search exit invariant violated");
+ // the binary search failed (hi and lo crossed) because spanToMap.Start did
+ // not intersect any source span. However, it may be that some part of spanToMap will
+ // intersect the next span, so we start our scan one span further along.
+
+ // another way to think of this: if the binary search failed, mid will designate either the source span
+ // to the left of the gap containing start or to the right of the gap containing start. This case
+ // is where orderedSources[mid] lies to the left of the gap, and we don't want the loop below to blow out on
+ // the first iteration if spanToMap intersects orderedSources[mid+1].
+ mid++;
+ }
+
+ for (int rover = mid; rover < orderedSources.Count; ++rover)
+ {
+ Span? s = spanToMap.Intersection(orderedSources[rover].sourceSpan);
+ if (!s.HasValue)
+ {
+ Debug.Assert(orderedSources[rover].sourceSpan.Start > spanToMap.End);
+ break;
+ }
+ if (s.Value.Length > 0 || spanToMap.Length == 0)
+ {
+ result.Add(new Span(orderedSources[rover].projectedPosition + (s.Value.Start - orderedSources[rover].sourceSpan.Start), s.Value.Length));
+ }
+ }
+
+ return new ReadOnlyCollection<Span>(result);
+ }
+
+ public override SnapshotPoint MapToSourceSnapshot(int position)
+ {
+ if (position < 0 || position > this.totalLength)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ ReadOnlyCollection<SnapshotPoint> points = this.MapInsertionPointToSourceSnapshots(position, this.projectionBuffer.literalBuffer); // should this be conditional on writable literal buffer?
+ if (points.Count == 1)
+ {
+ return points[0];
+ }
+ else if (this.projectionBuffer.resolver == null)
+ {
+ return points[points.Count - 1];
+ }
+ else
+ {
+ return points[this.projectionBuffer.resolver.GetTypicalInsertionPosition(new SnapshotPoint(this, position), points)];
+ }
+ }
+
+ public override SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity)
+ {
+ if (position < 0 || position > this.Length)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+
+ int rover = affinity == PositionAffinity.Predecessor ? FindLowestSpanIndexOfPosition(position)
+ : FindHighestSpanIndexOfPosition(position);
+ if (rover < 0)
+ {
+ Debug.Assert(this.sourceSpans.Count == 0);
+ throw new InvalidOperationException();
+ }
+
+ SnapshotSpan roverSpan = this.sourceSpans[rover];
+ return roverSpan.Start + (position - cumulativeLengths[rover]);
+ }
+
+ public override SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint sourcePoint, PositionAffinity affinity)
+ {
+ if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor)
+ {
+ throw new ArgumentOutOfRangeException("affinity");
+ }
+
+ List<InvertedSource> orderedSources;
+ if (!this.sourceSnapshotMap.TryGetValue(sourcePoint.Snapshot, out orderedSources))
+ {
+ throw new ArgumentException("The point does not belong to a source snapshot of the projection snapshot");
+ }
+
+ int position = sourcePoint.Position;
+ SnapshotPoint? candidatePoint = null;
+
+ // binary search for source span containing pointToMap
+ int lo = 0;
+ int hi = orderedSources.Count - 1;
+ while (lo <= hi)
+ {
+ int mid = (lo + hi) / 2;
+ Span sourceSpan = orderedSources[mid].sourceSpan;
+
+ if (position < sourceSpan.Start)
+ {
+ hi = mid - 1;
+ }
+ else if (position > sourceSpan.End)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ candidatePoint = new SnapshotPoint(this, orderedSources[mid].projectedPosition + sourcePoint.Position - sourceSpan.Start);
+
+ if ((position > sourceSpan.Start && position < sourceSpan.End) ||
+ (position == sourceSpan.Start && affinity == PositionAffinity.Successor) ||
+ (position == sourceSpan.End && affinity == PositionAffinity.Predecessor))
+ {
+ // unambiguous
+ return candidatePoint;
+ }
+
+ if (position == sourceSpan.Start)
+ {
+ hi = mid - 1;
+ }
+ else if (position == sourceSpan.End)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ Debug.Fail("Ambiguous point mapping unexpected condition");
+ break; // we have a decent answer
+ }
+ }
+ }
+
+ return candidatePoint;
+ }
+
+ /// <summary>
+ /// Map insertion point in projection buffer into set of insertion points in source buffers. The
+ /// result will have only one element unless the insertion is at the boundary of source spans, in which
+ /// case there can be two (or more if empty source spans appear at the insertion location).
+ /// </summary>
+ internal override ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer)
+ {
+ if (position < 0 || position > this.Length)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+
+ int rover = FindLowestSpanIndexOfPosition(position);
+
+ SnapshotSpan sourceSpan = this.sourceSpans[rover];
+ if (position < cumulativeLengths[rover + 1])
+ {
+ // point is not on a seam
+ FrugalList<SnapshotPoint> singleResult = new FrugalList<SnapshotPoint>();
+ singleResult.Add(sourceSpan.Start + (position - this.cumulativeLengths[rover]));
+ return new ReadOnlyCollection<SnapshotPoint>(singleResult);
+ }
+ else
+ {
+ // point is at the boundary of source spans (this includes being at the
+ // very beginning or end of the buffer, but that will work out OK).
+ var sourceInsertionPoints = new FrugalList<SnapshotPoint>();
+
+ // include the end point of the source span on the left
+ var firstSnapshotPoint = new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.End);
+ if (sourceSpan.Snapshot.TextBuffer != excludedBuffer)
+ {
+ sourceInsertionPoints.Add(firstSnapshotPoint);
+ }
+
+ // include all consecutive source spans of zero length (typically there are none of these)
+ while (++rover < this.sourceSpans.Count && this.cumulativeLengths[rover] == this.cumulativeLengths[rover + 1])
+ {
+ sourceSpan = this.sourceSpans[rover];
+ if (sourceSpan.Snapshot.TextBuffer != excludedBuffer)
+ {
+ sourceInsertionPoints.Add(new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.Start));
+ }
+ }
+
+ // include first nonzero length source span (if any)
+ if (rover < this.sourceSpans.Count)
+ {
+ sourceSpan = this.sourceSpans[rover];
+ if (sourceSpan.Snapshot.TextBuffer != excludedBuffer)
+ {
+ sourceInsertionPoints.Add(new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.Start));
+ }
+ }
+
+ if (sourceInsertionPoints.Count == 0)
+ {
+ // Where position falls in the seam between two (or more if they are 0 length) spans from excludedBuffer and there
+ // are no snapshot points from the "real" buffers. In this case, the best thing to do is simply return the span from
+ // the excluded buffer (which is consistent with our behavior when position falls inside the middle of an excluded span).
+ sourceInsertionPoints.Add(firstSnapshotPoint);
+ }
+
+ return new ReadOnlyCollection<SnapshotPoint>(sourceInsertionPoints);
+ }
+ }
+ #endregion
+
+ #region Search
+ /// <summary>
+ /// Finds the highest index of the source span that intersects <paramref name="position"/>. This means
+ /// that if the position is on a seam, the span to the "right" of the seam will be returned, and if
+ /// there is a sequence of empty spans at the position, the index of the successor of the last of them will be
+ /// returned.
+ /// </summary>
+ internal int FindHighestSpanIndexOfPosition(int position)
+ {
+ int lo = 0;
+ int hi = this.sourceSpans.Count - 1;
+ while (lo <= hi)
+ {
+ int mid = (lo + hi) / 2;
+ if (position < this.cumulativeLengths[mid])
+ {
+ hi = mid - 1;
+ }
+ else if (position >= this.cumulativeLengths[mid + 1])
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ // sourceSpans[mid] contains position
+ return mid;
+ }
+ }
+ Debug.Assert(position == this.Length);
+ return this.sourceSpans.Count - 1;
+ }
+
+ /// <summary>
+ /// Finds the lowest index of the source span that intersects <paramref name="position"/>. This means
+ /// that if the position is on a seam, the span to the "left" of the seam will be returned, and if
+ /// there is a sequence of empty spans at the position, the index of the first of them will be
+ /// returned.
+ /// </summary>
+ internal int FindLowestSpanIndexOfPosition(int position)
+ {
+ int lo = 0;
+ int hi = this.sourceSpans.Count - 1;
+ while (lo <= hi)
+ {
+ int mid = (lo + hi) / 2;
+ if (position < this.cumulativeLengths[mid] || (mid > 0 && position == this.cumulativeLengths[mid]))
+ {
+ hi = mid - 1;
+ }
+ else if (position > this.cumulativeLengths[mid + 1])
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ // sourceSpans[mid] contains position (or it is at the end of the span)
+ return mid;
+ }
+ }
+ Debug.Assert(position == this.Length);
+ return this.sourceSpans.Count - 1;
+ }
+
+ internal int FindLowestSpanIndexOfLineNumber(int lineNumber)
+ {
+ int lo = 0;
+ int hi = this.sourceSpans.Count - 1;
+ while (lo <= hi)
+ {
+ int mid = (lo + hi) / 2;
+ if (lineNumber <= this.cumulativeLineBreakCounts[mid] && mid > 0)
+ {
+ hi = mid - 1;
+ }
+ else if (lineNumber > this.cumulativeLineBreakCounts[mid + 1])
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ // sourceSpans[mid] contains position
+ return mid;
+ }
+ }
+ Debug.Assert(lineNumber == this.LineCount - 1);
+ return this.sourceSpans.Count - 1;
+ }
+
+ #endregion
+
+ #region Diagnostic Support
+ private string LocalToString()
+ {
+ // need a non-virtual form to call from the constructor
+ System.Text.StringBuilder image = new System.Text.StringBuilder();
+ image.AppendFormat(System.Globalization.CultureInfo.InvariantCulture,
+ "Snapshot {0,10} V{1}\r\n", TextUtilities.GetTagOrContentType(this.projectionBuffer), this.version.VersionNumber);
+ int cumulativeLength = 0;
+ for (int s = 0; s < this.sourceSpans.Count; ++s)
+ {
+ SnapshotSpan sourceSpan = this.sourceSpans[s];
+ image.AppendFormat(System.Globalization.CultureInfo.InvariantCulture,
+ "{0,12} {1,10} {2,4} {3,12} {4}\r\n",
+ new Span(cumulativeLength, sourceSpan.Length),
+ TextUtilities.GetTagOrContentType(sourceSpan.Snapshot.TextBuffer),
+ "V" + sourceSpan.Snapshot.Version.VersionNumber.ToString(),
+ sourceSpan.Span,
+ TextUtilities.Escape(sourceSpan.GetText()));
+ cumulativeLength += sourceSpan.Length;
+ }
+ return image.ToString();
+ }
+
+ public override string ToString()
+ {
+ return LocalToString();
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs
new file mode 100644
index 0000000..06b0306
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs
@@ -0,0 +1,86 @@
+//
+// 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.Projection.Implementation
+{
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal class ProjectionSpanToNormalizedChangeConverter
+ {
+ private INormalizedTextChangeCollection normalizedChanges;
+ private bool computed = false;
+ private int textPosition;
+ private ProjectionSpanDiffer differ;
+ private ITextSnapshot currentSnapshot;
+
+ public ProjectionSpanToNormalizedChangeConverter(ProjectionSpanDiffer differ,
+ int textPosition,
+ ITextSnapshot currentSnapshot)
+ {
+ this.differ = differ;
+ this.textPosition = textPosition;
+ this.currentSnapshot = currentSnapshot;
+ }
+
+ public INormalizedTextChangeCollection NormalizedChanges
+ {
+ get
+ {
+ if (!computed)
+ {
+ ConstructChanges();
+ computed = true;
+ }
+ return this.normalizedChanges;
+ }
+ }
+
+ #region Private helpers
+
+ private void ConstructChanges()
+ {
+ IDifferenceCollection<SnapshotSpan> diffs = differ.GetDifferences();
+
+ List<TextChange> changes = new List<TextChange>();
+ int pos = this.textPosition;
+
+ // each difference generates a text change
+ foreach (Difference diff in diffs)
+ {
+ pos += GetMatchSize(differ.DeletedSpans, diff.Before);
+ TextChange change = TextChange.Create(pos,
+ BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.DeletedSpans, diff.Left),
+ BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.InsertedSpans, diff.Right),
+ this.currentSnapshot);
+ changes.Add(change);
+ pos += change.OldLength;
+ }
+ this.normalizedChanges = NormalizedTextChangeCollection.Create(changes);
+ }
+
+ private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match)
+ {
+ int size = 0;
+ if (match != null)
+ {
+ Span extent = match.Left;
+ for (int s = extent.Start; s < extent.End; ++s)
+ {
+ size += spans[s].Length;
+ }
+ }
+ return size;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs b/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs
new file mode 100644
index 0000000..5510114
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs
@@ -0,0 +1,63 @@
+//
+// 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.Projection.Implementation
+{
+ internal struct ProjectionLineInfo
+ {
+ public int lineNumber;
+ public int start;
+ public int end; // excluding line break
+ public int lineBreakLength;
+ public bool startComplete;
+ public bool endComplete;
+
+ //public override string ToString()
+ //{
+ // System.Text.StringBuilder sb = new System.Text.StringBuilder("<");
+ // sb.Append("line# ");
+ // sb.Append(lineNumber);
+ // sb.Append(" start ");
+ // sb.Append(start);
+ // sb.Append(startComplete ? "!" : "?");
+ // sb.Append(" end ");
+ // sb.Append(end);
+ // sb.Append(endComplete ? "!" : "?");
+ // sb.Append(" lbl ");
+ // sb.Append(lineBreakLength);
+ // sb.Append(">");
+ // return sb.ToString();
+ //}
+ }
+
+ internal enum ProjectionLineCalculationState
+ {
+ /// <summary>
+ /// We are searching for the map node containing the requested position.
+ /// </summary>
+ Primary,
+
+ /// <summary>
+ /// The primary node has been found, but it did not contain the line break signifying the
+ /// end of the line, so we look to the right to discover the tail of the line.
+ /// </summary>
+ Append,
+
+ /// <summary>
+ /// The primary node has been found, but it did not contain the line break signifying the
+ /// end of the previous line, so we look to the left to discover the head of the line.
+ /// </summary>
+ Prepend,
+
+ /// <summary>
+ /// The primary node has been found, but it contained neither the line break signifying the
+ /// end of the previous line nor the line break signifying the end of the current line, so we
+ /// look both to the left and right to discover the head and tail of the line.
+ /// </summary>
+ Bipend
+ }
+}
diff --git a/src/Text/Impl/TextModel/Projection/WeakEventHook.cs b/src/Text/Impl/TextModel/Projection/WeakEventHook.cs
new file mode 100644
index 0000000..043892d
--- /dev/null
+++ b/src/Text/Impl/TextModel/Projection/WeakEventHook.cs
@@ -0,0 +1,101 @@
+//
+// 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.Projection.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Text;
+
+ using Microsoft.VisualStudio.Text.Implementation;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using System.Collections.ObjectModel;
+
+ /// <summary>
+ /// This class is intended to manage the all the references from a projected buffer (the source buffer) back to the
+ /// projection buffer (the target buffer) doing the projection. You can have a scenario where someone creates a projection
+ /// buffer and then forgets about it. Unfortunately the projection buffer is kept alive by the events it has hooked on
+ /// the source buffer so we get a memory leak.
+ ///
+ /// The fix here is to gather all the references into one place so that we don't keep the target buffer alive
+ /// simply to maintain state no one is interested in.
+ /// </summary>
+ internal class WeakEventHook
+ {
+ private readonly WeakReference<BaseProjectionBuffer> _targetBuffer;
+ private BaseBuffer _sourceBuffer;
+
+ public WeakEventHook(BaseProjectionBuffer targetBuffer, BaseBuffer sourceBuffer)
+ {
+ _targetBuffer = new WeakReference<BaseProjectionBuffer>(targetBuffer);
+ _sourceBuffer = sourceBuffer;
+
+ sourceBuffer.ChangedImmediate += OnSourceTextChanged;
+ sourceBuffer.ContentTypeChangedImmediate += OnSourceBufferContentTypeChanged;
+ sourceBuffer.ReadOnlyRegionsChanged += OnSourceBufferReadOnlyRegionsChanged;
+ }
+
+ public BaseBuffer SourceBuffer { get { return _sourceBuffer; } }
+
+ public BaseProjectionBuffer GetTargetBuffer() // Not a property since it has side-effects
+ {
+ BaseProjectionBuffer targetBuffer;
+ if (_targetBuffer.TryGetTarget(out targetBuffer))
+ {
+ return targetBuffer;
+ }
+
+ // The target buffer that was listening to events on the source buffer has died (no one was using it).
+ // Dead buffers tell no tales so they get to stop listening to tales as well. Unsubscribe from the
+ // events it hooked on the source buffer.
+ this.UnsubscribeFromSourceBuffer();
+ return null;
+ }
+
+ private void OnSourceTextChanged(object sender, TextContentChangedEventArgs e)
+ {
+ BaseProjectionBuffer targetBuffer = this.GetTargetBuffer();
+ if (targetBuffer != null)
+ {
+ targetBuffer.OnSourceTextChanged(sender, e);
+ }
+ }
+
+ private void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e)
+ {
+ BaseProjectionBuffer targetBuffer = this.GetTargetBuffer();
+ if (targetBuffer != null)
+ {
+ targetBuffer.OnSourceBufferContentTypeChanged(sender, e);
+ }
+ }
+
+ private void OnSourceBufferReadOnlyRegionsChanged(object sender, SnapshotSpanEventArgs e)
+ {
+ BaseProjectionBuffer targetBuffer = this.GetTargetBuffer();
+ if (targetBuffer != null)
+ {
+ targetBuffer.OnSourceBufferReadOnlyRegionsChanged(sender, e);
+ }
+ }
+
+ public void UnsubscribeFromSourceBuffer()
+ {
+ if (_sourceBuffer != null)
+ {
+ _sourceBuffer.ChangedImmediate -= OnSourceTextChanged;
+ _sourceBuffer.ContentTypeChangedImmediate -= OnSourceBufferContentTypeChanged;
+ _sourceBuffer.ReadOnlyRegionsChanged -= OnSourceBufferReadOnlyRegionsChanged;
+
+ _sourceBuffer = null;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/ReadOnlyRegion.cs b/src/Text/Impl/TextModel/ReadOnlyRegion.cs
new file mode 100644
index 0000000..27c2806
--- /dev/null
+++ b/src/Text/Impl/TextModel/ReadOnlyRegion.cs
@@ -0,0 +1,91 @@
+//
+// 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;
+
+ /// <summary>
+ /// A possibly empty read only region of text.
+ /// </summary>
+ internal partial class ReadOnlyRegion : IReadOnlyRegion
+ {
+ #region Private Members
+
+ private readonly ITrackingSpan _trackingSpan;
+ private readonly EdgeInsertionMode _edgeInsertionMode;
+
+ #endregion // Private Members
+
+ #region Constructors
+
+ /// <summary>
+ /// Creates a ReadOnlyRegionHandle for the given buffer and span.
+ /// </summary>
+ /// <param name="version">
+ /// The <see cref="TextVersion"/> with which this read only region is associated.
+ /// </param>
+ /// <param name="span">
+ /// The span of interest.
+ /// </param>
+ /// <param name="trackingMode">
+ /// Specifies the tracking behavior of the read only region.
+ /// </param>
+ /// <param name="edgeInsertionMode">
+ /// Specifies if insertions should be allowed at the edges
+ /// </param>
+ /// <remarks>
+ /// Don't call this constructor with invalid parameters. It doesn't verify all of them.
+ /// </remarks>
+ internal ReadOnlyRegion(TextVersion version, Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode, DynamicReadOnlyRegionQuery callback)
+ {
+ _edgeInsertionMode = edgeInsertionMode;
+ // TODO: change to simple forward tracking text span
+ _trackingSpan = new ForwardFidelityTrackingSpan(version, span, trackingMode);
+ QueryCallback = callback;
+ }
+
+ #endregion
+
+ public DynamicReadOnlyRegionQuery QueryCallback { get; private set; }
+
+ #region Public properties
+
+ /// <summary>
+ /// Span of text marked read only by this region.
+ /// </summary>
+ public ITrackingSpan Span
+ {
+ get { return _trackingSpan; }
+ }
+
+ /// <summary>
+ /// The edge insertion behavior of this read only region.
+ /// </summary>
+ public EdgeInsertionMode EdgeInsertionMode
+ {
+ get { return _edgeInsertionMode; }
+ }
+
+ #endregion
+
+ #region Overrides
+ /// <summary>
+ /// String representation of the ReadOnlyRegion.
+ /// </summary>
+ public override string ToString()
+ {
+ Span currentTrackingSpan = _trackingSpan.GetSpan(_trackingSpan.TextBuffer.CurrentSnapshot);
+
+ return string.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "RO: {2}{0}..{1}{3}", currentTrackingSpan.Start, currentTrackingSpan.End, _edgeInsertionMode == EdgeInsertionMode.Deny ? "[" : "(",
+ _edgeInsertionMode == EdgeInsertionMode.Deny ? "]" : ")");
+ }
+ #endregion
+ }
+}
+
diff --git a/src/Text/Impl/TextModel/ReadOnlySpan.cs b/src/Text/Impl/TextModel/ReadOnlySpan.cs
new file mode 100644
index 0000000..27994e6
--- /dev/null
+++ b/src/Text/Impl/TextModel/ReadOnlySpan.cs
@@ -0,0 +1,147 @@
+//
+// 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.Collections.Generic;
+
+ /// <summary>
+ /// A span which tracks whether or not its edges can be inserted at.
+ /// </summary>
+ internal class ReadOnlySpan : ForwardFidelityTrackingSpan
+ {
+ #region Private members
+ private readonly EdgeInsertionMode _startEdgeInsertionMode;
+ private readonly EdgeInsertionMode _endEdgeInsertionMode;
+ #endregion
+
+ #region Constructors
+ internal ReadOnlySpan(ITextVersion version, Span span, SpanTrackingMode trackingMode, EdgeInsertionMode startEdgeInsertionMode, EdgeInsertionMode endEdgeInsertionMode)
+ : base(version, span, trackingMode)
+ {
+ _startEdgeInsertionMode = startEdgeInsertionMode;
+ _endEdgeInsertionMode = endEdgeInsertionMode;
+ }
+
+ internal ReadOnlySpan(ITextVersion version, IReadOnlyRegion readOnlyRegion)
+ : base(version, readOnlyRegion.Span.GetSpan(version), readOnlyRegion.Span.TrackingMode)
+ {
+ _startEdgeInsertionMode = readOnlyRegion.EdgeInsertionMode;
+ _endEdgeInsertionMode = readOnlyRegion.EdgeInsertionMode;
+ }
+ #endregion
+
+ #region Public properties
+
+ /// <summary>
+ /// Whether this span allows insertions on the start edge
+ /// </summary>
+ public EdgeInsertionMode StartEdgeInsertionMode
+ {
+ get { return _startEdgeInsertionMode; }
+ }
+
+ /// <summary>
+ /// Whether this span allows insertions on the end edge
+ /// </summary>
+ public EdgeInsertionMode EndEdgeInsertionMode
+ {
+ get { return _endEdgeInsertionMode; }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Determine if a replace of a particular span will be allowed by this read only region.
+ ///
+ /// Validate parameters before calling this method.
+ ///
+ /// Zero length read only regions only disallow inserts.
+ /// </summary>
+ /// <param name="span">The span to check to see if a change would be allowed.</param>
+ /// <param name="textSnapshot">The snapshot to check to see if replace is allowed.</param>
+ /// <returns>Whether or not the change is allowed.</returns>
+ public bool IsReplaceAllowed(Span span, ITextSnapshot textSnapshot)
+ {
+ // Check to see if insert is allowed since we are doing a replace of a zero length span.
+ if (span.Length == 0)
+ {
+ return IsInsertAllowed(span.Start, textSnapshot);
+ }
+
+ Span currentSpan = this.GetSpan(textSnapshot);
+
+ // Zero length read only regions only
+ // disallow inserts.
+ if (currentSpan.Length == 0)
+ {
+ return true;
+ }
+
+ // If the span overlaps this read only
+ // region, then the change will not be allowed.
+ if ((currentSpan == span) || currentSpan.OverlapsWith(span))
+ {
+ return false;
+ }
+
+ // Nothing disallows this change
+ return true;
+ }
+
+ /// <summary>
+ /// Determine if an insert is allowed by this read only region.
+ ///
+ /// Validate parameters before calling this method.
+ /// </summary>
+ /// <param name="position">The position to check if the insert is allowed.</param>
+ /// <param name="textSnapshot">The text snapshot to check the position relative to.</param>
+ /// <returns></returns>
+ public bool IsInsertAllowed(int position, ITextSnapshot textSnapshot)
+ {
+ Span currentSpan = this.GetSpan(textSnapshot);
+
+ // Does this position fall in the middle of this span?
+ if ((currentSpan.Start < position) && (currentSpan.End > position))
+ {
+ return false;
+ }
+
+ // If edge insertions are prohibited on the start edge
+ // and an insert is occurring on the start edge, the insert
+ // is not allowed.
+ if (this.StartEdgeInsertionMode == EdgeInsertionMode.Deny)
+ {
+ if (position == currentSpan.Start)
+ {
+ return false;
+ }
+ }
+
+ // If edge insertions are prohibited on the end edge
+ // and an insert is occurring on the end edge, the change
+ // is not allowed.
+ if (EndEdgeInsertionMode == EdgeInsertionMode.Deny)
+ {
+ if (position == currentSpan.End)
+ {
+ return false;
+ }
+ }
+
+ // Nothing disallows this change
+ return true;
+ }
+
+ #endregion
+ }
+}
+
diff --git a/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs b/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs
new file mode 100644
index 0000000..2bca68d
--- /dev/null
+++ b/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs
@@ -0,0 +1,385 @@
+//
+// 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.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ /// <summary>
+ /// A collection of read only spans that are sorted by start position, with adjacent and overlapping spans combined.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// If two snapshots have the same read only regions, then they will have the same read only span collection.
+ /// </para>
+ /// <para>
+ /// Asking a ReadOnlySpanCollection if it intersects an ITrackingSpan or ITrackingPoint with a snapshot before
+ /// the first one the ReadOnlySpanCollection was created with is undefined.
+ /// </para>
+ /// </remarks>
+ internal class ReadOnlySpanCollection : ReadOnlyCollection<ReadOnlySpan>
+ {
+ readonly List<IReadOnlyRegion> regionsWithActions;
+
+ internal IEnumerable<ReadOnlySpan> QueryAllEffectiveReadOnlySpans(ITextVersion version)
+ {
+ foreach (var span in this)
+ yield return span;
+
+ foreach (var regionWithAction in this.regionsWithActions)
+ if (regionWithAction.QueryCallback(isEdit: false))
+ yield return new ReadOnlySpan(version, regionWithAction);
+ }
+
+ /// <summary>
+ /// Construct a ReadOnlySpanCollection that contains everything in a list of spans.
+ /// </summary>
+ /// <param name="regions">ReadOnlyRegions</param>
+ /// <param name="version">The version this span collection applies to.</param>
+ /// <remarks>
+ /// <para>The list of spans will be sorted and normalized (overlapping and adjoining spans will be combined).</para>
+ /// </remarks>
+ /// <exception cref="ArgumentNullException"><paramref name="regions"/> is null.</exception>
+ internal ReadOnlySpanCollection(TextVersion version, IEnumerable<IReadOnlyRegion> regions)
+ : base(NormalizeSpans(version, regions))
+ {
+ regionsWithActions = regions.Where(region => region.QueryCallback != null).ToList();
+ }
+
+ internal bool IsReadOnly(int position, ITextSnapshot textSnapshot, bool notify)
+ {
+ foreach (var region in this.regionsWithActions)
+ {
+ if (!IsEditAllowed(region, position, textSnapshot))
+ {
+ if (region.QueryCallback(notify))
+ return true;
+ }
+ }
+
+ // O(n) implementation which is much more straightforward
+ for (int i = 0; i < this.Count; i++)
+ {
+ if (!this[i].IsInsertAllowed(position, textSnapshot))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ // This is the O(lg n) implementation
+ #region Binary search implementation
+ /*
+ if (this.Count == 0)
+ {
+ return false;
+ }
+
+ // We know that the spans don't overlap, so we can do a binary search here.
+ int start = this.Count / 2;
+ ReadOnlySpan startSpan = this[start];
+ while (true)
+ {
+ Span rawSpan = startSpan.GetSpan(textSnapshot);
+ int newStart = start;
+ if (rawSpan.End < position)
+ {
+ newStart = start + 1 + start / 2;
+ }
+ else if (rawSpan.Start > position)
+ {
+ newStart = start / 2;
+ }
+ else
+ {
+ break;
+ }
+
+ if (newStart == start)
+ {
+ Debug.Assert(!startSpan.GetSpan(textSnapshot).Contains(position));
+ return false;
+ }
+
+ if (newStart < this.Count)
+ {
+ start = newStart;
+ startSpan = this[start];
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (!startSpan.IsInsertAllowed(position, textSnapshot))
+ {
+ return true;
+ }
+
+ if (start > 0)
+ {
+ if (!this[start - 1].IsInsertAllowed(position, textSnapshot))
+ {
+ return true;
+ }
+ }
+
+ if (start < this.Count - 1)
+ {
+ return !this[start + 1].IsInsertAllowed(position, textSnapshot);
+ }
+
+ return false;
+ * */
+ #endregion
+ }
+
+ private static bool IsEditAllowed(IReadOnlyRegion region, int position, ITextSnapshot textSnapshot)
+ {
+ return new ReadOnlySpan(textSnapshot.Version, region).IsInsertAllowed(position, textSnapshot);
+ }
+
+ private static bool IsEditAllowed(IReadOnlyRegion region, Span span, ITextSnapshot textSnapshot)
+ {
+ return new ReadOnlySpan(textSnapshot.Version, region).IsReplaceAllowed(span, textSnapshot);
+ }
+
+ internal bool IsReadOnly(Span span, ITextSnapshot textSnapshot, bool notify)
+ {
+ foreach (var region in this.regionsWithActions)
+ {
+ if (!IsEditAllowed(region, span, textSnapshot))
+ {
+ if (region.QueryCallback(notify))
+ return true;
+ }
+ }
+
+ // O(n) implementation which is much more straightforward
+ for (int i = 0; i < this.Count; i++)
+ {
+ if (!this[i].IsReplaceAllowed(span, textSnapshot))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ // This is the O(lg n) implementation
+ #region Binary search implementation
+
+ /*
+ if (this.Count == 0)
+ {
+ return false;
+ }
+
+ // We know that the spans don't overlap, so we can do a binary search here.
+ int start = this.Count / 2;
+ ReadOnlySpan startSpan = this[start];
+ while (true)
+ {
+ Span rawSpan = startSpan.GetSpan(textSnapshot);
+ int newStart = start;
+ if (rawSpan.End < span.Start)
+ {
+ newStart = start + 1 + start / 2;
+ }
+ else if (rawSpan.Start > span.End)
+ {
+ newStart = start / 2;
+ }
+ else
+ {
+ break;
+ }
+
+ if (newStart == start)
+ {
+ Debug.Assert(!startSpan.GetSpan(textSnapshot).Contains(span));
+ return false;
+ }
+
+ if (newStart < this.Count)
+ {
+ start = newStart;
+ startSpan = this[start];
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (!startSpan.IsReplaceAllowed(span, textSnapshot))
+ {
+ return true;
+ }
+
+ if (start > 0)
+ {
+ if (!this[start - 1].IsReplaceAllowed(span, textSnapshot))
+ {
+ return true;
+ }
+ }
+
+ if (start < this.Count - 1)
+ {
+ return !this[start + 1].IsReplaceAllowed(span, textSnapshot);
+ }
+
+ return false;
+ */
+ #endregion
+ }
+
+ private static IList<ReadOnlySpan> NormalizeSpans(TextVersion version, IEnumerable<IReadOnlyRegion> regions)
+ {
+ List<IReadOnlyRegion> sorted = new List<IReadOnlyRegion>(regions.Where(region => region.QueryCallback == null));
+
+ if (sorted.Count == 0)
+ {
+ return new FrugalList<ReadOnlySpan>();
+ }
+ else if (sorted.Count == 1)
+ {
+ return new FrugalList<ReadOnlySpan>() {new ReadOnlySpan(version, sorted[0])};
+ }
+ else
+ {
+ sorted.Sort((s1, s2) => s1.Span.GetSpan(version).Start.CompareTo(s2.Span.GetSpan(version).Start));
+
+ List<ReadOnlySpan> normalized = new List<ReadOnlySpan>(sorted.Count);
+
+ int oldStart = sorted[0].Span.GetSpan(version).Start;
+ int oldEnd = sorted[0].Span.GetSpan(version).End;
+ EdgeInsertionMode oldStartEdgeInsertionMode = sorted[0].EdgeInsertionMode;
+ EdgeInsertionMode oldEndEdgeInsertionMode = sorted[0].EdgeInsertionMode;
+ SpanTrackingMode oldSpanTrackingMode = sorted[0].Span.TrackingMode;
+ for (int i = 1; (i < sorted.Count); ++i)
+ {
+ int newStart = sorted[i].Span.GetSpan(version).Start;
+ int newEnd = sorted[i].Span.GetSpan(version).End;
+
+ // Since the new span's start occurs after the old span's end, we can just add the old span directly.
+ if (oldEnd < newStart)
+ {
+ normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode));
+ oldStart = newStart;
+ oldEnd = newEnd;
+ oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldSpanTrackingMode = sorted[i].Span.TrackingMode;
+ }
+ else
+ {
+ // The two read only regions start at the same position
+ if (newStart == oldStart)
+ {
+ // If one read only region denies edge insertions, combined they do as well
+ if (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Deny)
+ {
+ oldStartEdgeInsertionMode = EdgeInsertionMode.Deny;
+ }
+
+ // This is tricky. We want one span that will be inclusive tracking, and one that won't.
+ if (oldSpanTrackingMode != sorted[i].Span.TrackingMode)
+ {
+ // Since the read only regions cover the same exact span, the combined one will be edge inclusive tracking
+ if (oldEnd == newEnd)
+ {
+ oldSpanTrackingMode = SpanTrackingMode.EdgeInclusive;
+ }
+ else if (oldEnd < newEnd)
+ {
+ // Since the old span and new span don't have the same span tracking mode and don't end in the same position, we need to create a new span that is edge inclusive
+ // and deny inserts between it and the next span.
+ normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), SpanTrackingMode.EdgeInclusive, oldStartEdgeInsertionMode, EdgeInsertionMode.Deny));
+ oldStart = oldEnd; // Explicitly use the old end here since we want these spans to be adjacent
+ oldEnd = newEnd;
+ oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldSpanTrackingMode = sorted[i].Span.TrackingMode;
+ }
+ else
+ {
+ // Since the new span ends first, create a span that is edge inclusive tracking that ends at the the new span's end.
+ normalized.Add(new ReadOnlySpan(version, new Span(newStart, newEnd - newStart), SpanTrackingMode.EdgeInclusive, oldStartEdgeInsertionMode, EdgeInsertionMode.Deny));
+ oldStart = newEnd; // Explicitly use the new end here since we want these spans to be adjacent
+ }
+ }
+ }
+
+ if (oldEnd < newEnd)
+ {
+ // If the tracking modes are different then we need to create a new span
+ // with the old tracking mode, and start a new span with the new span tracking mode.
+ // Also, if the old end and the new start are identical and both edge insertion mode's
+ // are allow, then we need to create a new span.
+ if (((oldEnd == newStart)
+ &&
+ ((oldEndEdgeInsertionMode == EdgeInsertionMode.Allow) && (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Allow)))
+ ||
+ (oldSpanTrackingMode != sorted[i].Span.TrackingMode))
+ {
+ normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode));
+ oldStart = oldEnd; // Explicitly use the old end here since we want these spans to be adjacent.
+ oldEnd = newEnd;
+
+ // If we are splitting up the spans because of a change in tracking mode, then explicitly deny inserting between them
+ if (oldSpanTrackingMode != sorted[i].Span.TrackingMode)
+ {
+ oldStartEdgeInsertionMode = EdgeInsertionMode.Deny; // Explicitly use deny here since we don't want to allow insertions between these spans
+ }
+ else
+ {
+ oldStartEdgeInsertionMode = EdgeInsertionMode.Allow;
+ }
+ oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldSpanTrackingMode = sorted[i].Span.TrackingMode;
+ }
+ else
+ {
+ oldEnd = newEnd;
+ oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ }
+ }
+ else if (oldEnd == newEnd)
+ {
+ if (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Deny)
+ {
+ oldEndEdgeInsertionMode = EdgeInsertionMode.Deny;
+ }
+ if (oldSpanTrackingMode != sorted[i].Span.TrackingMode)
+ {
+ normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode));
+ oldStart = newEnd;
+ oldEnd = newEnd;
+ oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode;
+ oldSpanTrackingMode = sorted[i].Span.TrackingMode;
+ }
+ }
+ }
+ }
+ normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode));
+
+ return normalized;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/CharStream.cs b/src/Text/Impl/TextModel/Storage/CharStream.cs
new file mode 100644
index 0000000..2179e9d
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/CharStream.cs
@@ -0,0 +1,133 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ /// <summary>
+ /// Stream that converts between characters and bytes.
+ /// </summary>
+ internal class CharStream : Stream
+ {
+ char[] data;
+ int length; // *byte* length of data
+ int position = 0; // *byte* offset into data
+ byte? pendingByte;
+
+ public CharStream(char[] data, int length)
+ {
+ this.data = data;
+ this.length = 2 * length;
+ }
+
+ public override bool CanRead { get { return true; } }
+ public override bool CanSeek { get { return false; } }
+ public override bool CanWrite { get { return true; } }
+
+ public override void Flush()
+ {
+ }
+
+ public override long Length { get { return (int)this.length; } }
+
+ public override long Position
+ {
+ get { return this.position; }
+ set { throw new NotSupportedException(); }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int actualCount = Math.Min(count, this.length - this.position);
+ if (actualCount > 0)
+ {
+ int residue = actualCount;
+ byte hi, lo;
+ if (this.position % 2 == 1)
+ {
+ Split(this.data[this.position / 2], out hi, out lo);
+ buffer[offset++] = hi;
+ this.position++;
+ residue--;
+ }
+ for (int i = 0; i < residue / 2; ++i)
+ {
+ Split(this.data[this.position / 2], out hi, out lo);
+ buffer[offset++] = hi;
+ buffer[offset++] = lo;
+ this.position += 2;
+ }
+ if (residue % 2 == 1)
+ {
+ Split(this.data[this.position / 2], out hi, out lo);
+ buffer[offset++] = lo;
+ this.position++;
+ }
+ }
+ return actualCount;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (count == 0)
+ {
+ return;
+ }
+
+ if (this.pendingByte.HasValue)
+ {
+ // finish previous character
+ Debug.Assert(this.position % 2 == 1);
+ this.data[this.position / 2] = Make(this.pendingByte.Value, buffer[offset]);
+ this.position++;
+ offset++;
+ count--;
+ }
+
+ for (int i = 0; i < count / 2; ++i)
+ {
+ this.data[this.position / 2] = Make(buffer[offset], buffer[offset + 1]);
+ this.position += 2;
+ offset += 2;
+ }
+
+ if (count % 2 == 0)
+ {
+ this.pendingByte = null;
+ }
+ else
+ {
+ this.pendingByte = buffer[offset];
+ this.position++;
+ }
+ }
+
+ private char Make(byte hi, byte lo)
+ {
+ return (char)((hi << 8) | lo);
+ }
+
+ private void Split(char c, out byte hi, out byte lo)
+ {
+ hi = (byte)(c >> 8);
+ lo = (byte)(c & 255);
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/Compressor.cs b/src/Text/Impl/TextModel/Storage/Compressor.cs
new file mode 100644
index 0000000..43a65c1
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/Compressor.cs
@@ -0,0 +1,77 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal static class Compressor
+ {
+#if COMPRESSOR_TIMING
+ static int compressCount;
+ static int decompressCount;
+ static long compressMilliseconds;
+ static long decompressMilliseconds;
+ static long averageCompressMilliseconds;
+ static long averageDecompressMilliseconds;
+#endif
+ public static byte[] Compress(char[] buffer, int length)
+ {
+ byte[] result;
+#if COMPRESSOR_TIMING
+ Stopwatch watch = new Stopwatch();
+ watch.Start();
+#endif
+ using (var inflatedBytes = new CharStream(buffer, length))
+ {
+ using (var deflatedBytes = new MemoryStream(length / 9)) // guess size of compressed text
+ {
+ using (DeflateStream compress = new DeflateStream(deflatedBytes, CompressionMode.Compress))
+ {
+ inflatedBytes.CopyTo(compress);
+ }
+ result = deflatedBytes.GetBuffer();
+ }
+ }
+
+#if COMPRESSOR_TIMING
+ compressCount++;
+ compressMilliseconds += watch.ElapsedMilliseconds;
+ averageCompressMilliseconds = compressMilliseconds / compressCount;
+#endif
+ return result;
+ }
+
+ public static void Decompress(byte[] compressed, int length, char[] decompressed)
+ {
+#if COMPRESSOR_TIMING
+ Stopwatch watch = new Stopwatch();
+ watch.Start();
+#endif
+
+ using (var deflatedBytes = new MemoryStream(compressed))
+ {
+ using (var inflatedChars = new CharStream(decompressed, length))
+ {
+ using (DeflateStream decompress = new DeflateStream(deflatedBytes, CompressionMode.Decompress))
+ {
+ decompress.CopyTo(inflatedChars);
+ }
+ }
+ }
+
+#if COMPRESSOR_TIMING
+ decompressCount++;
+ decompressMilliseconds += watch.ElapsedMilliseconds;
+ averageDecompressMilliseconds = decompressMilliseconds / decompressCount;
+#endif
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/ILineBreaks.cs b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs
new file mode 100644
index 0000000..ccf1358
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ /// <summary>
+ /// Information about the line breaks contained in an <see cref="StringRebuilderForChars"/>.
+ /// </summary>
+ public interface ILineBreaks
+ {
+ /// <summary>
+ /// The number of line breaks in the <see cref="StringRebuilderForChars"/>.
+ /// </summary>
+ int Length { get; }
+
+ /// <summary>
+ /// The starting position of the <paramref name="index"/>th line break.
+ /// </summary>
+ int StartOfLineBreak(int index);
+
+ /// <summary>
+ /// The starting position of the <paramref name="index"/>th line break.
+ /// </summary>
+ int EndOfLineBreak(int index);
+ }
+
+ public interface ILineBreaksEditor : ILineBreaks
+ {
+ /// <summary>
+ /// Add a line break at <paramref name="start"/> with <paramref name="length"/>
+ /// </summary>
+ void Add(int start, int length);
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/LineBreakManager.cs b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs
new file mode 100644
index 0000000..2d3071b
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ public static class LineBreakManager
+ {
+ public readonly static ILineBreaks Empty = new ShortLineBreaksEditor(0);
+
+ public static ILineBreaksEditor CreateLineBreakEditor(int maxLength, int initialCapacity = 0)
+ {
+ return (maxLength < short.MaxValue)
+ ? (ILineBreaksEditor)(new ShortLineBreaksEditor(initialCapacity))
+ : (ILineBreaksEditor)(new IntLineBreaksEditor(initialCapacity));
+ }
+
+ public static ILineBreaks CreateLineBreaks(string source)
+ {
+ ILineBreaksEditor lineBreaks = null;
+
+ int index = 0;
+ while (index < source.Length)
+ {
+ int breakLength = TextUtilities.LengthOfLineBreak(source, index, source.Length);
+ if (breakLength == 0)
+ {
+ ++index;
+ }
+ else
+ {
+ if (lineBreaks == null)
+ lineBreaks = LineBreakManager.CreateLineBreakEditor(source.Length);
+
+ lineBreaks.Add(index, breakLength);
+ index += breakLength;
+ }
+ }
+
+ return lineBreaks ?? Empty;
+ }
+
+ private class ShortLineBreaksEditor : ILineBreaksEditor
+ {
+ private const ushort MaskForPosition = 0x7fff;
+ private const ushort MaskForLength = 0x8000;
+
+ private readonly static List<ushort> Empty = new List<ushort>(0);
+ private List<ushort> _lineBreaks = Empty;
+
+ public ShortLineBreaksEditor(int initialCapacity)
+ {
+ if (initialCapacity > 0)
+ _lineBreaks = new List<ushort>(initialCapacity);
+ }
+
+ public int Length => _lineBreaks.Count;
+
+ public int LengthOfLineBreak(int index)
+ {
+ return ((_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1);
+ }
+
+ public int StartOfLineBreak(int index)
+ {
+ return (int)(_lineBreaks[index] & MaskForPosition);
+ }
+ public int EndOfLineBreak(int index)
+ {
+ int lineBreak = _lineBreaks[index];
+ return (lineBreak & MaskForPosition) +
+ (((lineBreak & MaskForLength) != 0) ? 2 : 1);
+ }
+
+ public void Add(int start, int length)
+ {
+ if ((start < 0) || (start > short.MaxValue))
+ throw new ArgumentOutOfRangeException(nameof(start));
+ if ((length < 1) || (length > 2))
+ throw new ArgumentOutOfRangeException(nameof(length));
+
+ if (_lineBreaks == Empty)
+ _lineBreaks = new List<ushort>();
+
+ if (length == 1)
+ _lineBreaks.Add((ushort)start);
+ else if (length == 2)
+ _lineBreaks.Add((ushort)(start | MaskForLength));
+ }
+ }
+
+ private class IntLineBreaksEditor : ILineBreaksEditor
+ {
+ private const uint MaskForPosition = 0x7fffffff;
+ private const uint MaskForLength = 0x80000000;
+
+ private readonly static List<uint> Empty = new List<uint>(0);
+ private List<uint> _lineBreaks = Empty;
+
+ public IntLineBreaksEditor(int initialCapacity)
+ {
+ if (initialCapacity > 0)
+ _lineBreaks = new List<uint>(initialCapacity);
+ }
+
+ public int Length => _lineBreaks.Count;
+
+ public int LengthOfLineBreak(int index)
+ {
+ return (_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1;
+ }
+
+ public int StartOfLineBreak(int index)
+ {
+ return (int)(_lineBreaks[index] & MaskForPosition);
+ }
+
+ public int EndOfLineBreak(int index)
+ {
+ uint lineBreak = _lineBreaks[index];
+ return (int)((lineBreak & MaskForPosition) +
+ (((lineBreak & MaskForLength) != 0) ? 2 : 1));
+ }
+
+ public void Add(int start, int length)
+ {
+ if ((start < 0) || (start > int.MaxValue))
+ throw new ArgumentOutOfRangeException(nameof(start));
+ if ((length < 1) || (length > 2))
+ throw new ArgumentOutOfRangeException(nameof(length));
+
+ if (_lineBreaks == Empty)
+ _lineBreaks = new List<uint>();
+
+ if (length == 1)
+ _lineBreaks.Add((uint)start);
+ else if (length == 2)
+ _lineBreaks.Add((uint)(start | MaskForLength));
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/Page.cs b/src/Text/Impl/TextModel/Storage/Page.cs
new file mode 100644
index 0000000..5256373
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/Page.cs
@@ -0,0 +1,48 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ /// <summary>
+ /// Base class for a page that participates in an MRU list.
+ /// </summary>
+ internal class Page
+ {
+ private WeakReference<char[]> _uncompressedContents;
+ private byte[] _compressedContents;
+
+ public readonly int Length;
+ public readonly PageManager Manager;
+
+ public Page(PageManager manager, char[] contents, int length)
+ {
+ this.Manager = manager;
+ this.Length = length;
+
+ _uncompressedContents = new WeakReference<char[]>(contents);
+ _compressedContents = Compressor.Compress(contents, length);
+ }
+
+ public char[] Expand()
+ {
+ char[] contents;
+ if (!_uncompressedContents.TryGetTarget(out contents))
+ {
+ contents = new char[this.Length];
+ Compressor.Decompress(_compressedContents, this.Length, contents);
+
+ _uncompressedContents.SetTarget(contents);
+ }
+
+ this.Manager.UpdateMRU(this);
+ return contents;
+
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/PageManager.cs b/src/Text/Impl/TextModel/Storage/PageManager.cs
new file mode 100644
index 0000000..eca1287
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/PageManager.cs
@@ -0,0 +1,66 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Threading;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal class PageManager
+ {
+ // this class inherits from page so that it participates in the MRU list, of which it is the sentinel node.
+ private ImmutableList<Page> _mru = ImmutableList<Page>.Empty;
+ private readonly int _maxPages;
+
+ public PageManager()
+ {
+ _maxPages = TextModelOptions.CompressedStorageMaxLoadedPages;
+ }
+
+ public void UpdateMRU(Page page)
+ {
+ var oldMRU = Volatile.Read(ref _mru);
+ while (true)
+ {
+ ImmutableList<Page> newMRU;
+
+ int index = oldMRU.IndexOf(page);
+ if (index >= 0)
+ {
+ if (index == (oldMRU.Count - 1))
+ {
+ // Page is already at the top of the MRU so nothing needs to be done.
+ return;
+ }
+
+ // Was in the list, but not at the top. Remove it in preparation for adding it later.
+ newMRU = oldMRU.RemoveAt(index);
+ }
+ else if (oldMRU.Count >= _maxPages)
+ {
+ // Wasn't in the list and the list is full. Remove the oldest in preparation for adding it later.
+ newMRU = oldMRU.RemoveAt(0);
+ }
+ else
+ {
+ newMRU = oldMRU;
+ }
+
+ newMRU = newMRU.Add(page);
+
+ var result = Interlocked.CompareExchange(ref _mru, newMRU, oldMRU);
+ if (result == oldMRU)
+ return;
+
+ oldMRU = result;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Storage/TextImageLoader.cs b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs
new file mode 100644
index 0000000..428d29c
--- /dev/null
+++ b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs
@@ -0,0 +1,199 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal static class TextImageLoader
+ {
+ public const int BlockSize = 16384;
+
+ internal static StringRebuilder Load(TextReader reader, long fileSize, string id,
+ out bool hasConsistentLineEndings, out int longestLineLength,
+ int blockSize = 0,
+ int minCompressedBlockSize = TextImageLoader.BlockSize) // Exposed for unit tests
+ {
+ LineEndingState lineEnding = LineEndingState.Unknown;
+ int currentLineLength = 0;
+ longestLineLength = 0;
+
+ bool useCompressedStringRebuilders = (fileSize >= TextModelOptions.CompressedStorageFileSizeThreshold);
+ if (blockSize == 0)
+ blockSize = useCompressedStringRebuilders ? TextModelOptions.CompressedStoragePageSize : TextImageLoader.BlockSize;
+
+ PageManager pageManager = null;
+ char[] buffer;
+ if (useCompressedStringRebuilders)
+ {
+ pageManager = new PageManager();
+ buffer = new char[blockSize];
+ }
+ else
+ {
+ buffer = TextImageLoader.AcquireBuffer(blockSize);
+ }
+
+ StringRebuilder content = StringRebuilderForChars.Empty;
+ try
+ {
+ while (true)
+ {
+ int read = TextImageLoader.LoadNextBlock(reader, buffer);
+
+ if (read == 0)
+ break;
+
+ var lineBreaks = LineBreakManager.CreateLineBreakEditor(read);
+ TextImageLoader.ParseBlock(buffer, read, lineBreaks, ref lineEnding, ref currentLineLength, ref longestLineLength);
+
+ char[] bufferForStringBuilder = buffer;
+ if (read < (buffer.Length / 2))
+ {
+ // We read far less characters than buffer so copy the contents to a new buffer and reuse the original buffer.
+ bufferForStringBuilder = new char[read];
+ Array.Copy(buffer, bufferForStringBuilder, read);
+ }
+ else
+ {
+ // We're using most of bufferForStringRebuilder so allocate a new block for the next chunk.
+ buffer = new char[blockSize];
+ }
+
+ var newContent = (useCompressedStringRebuilders && (read > minCompressedBlockSize))
+ ? StringRebuilderForCompressedChars.Create(new Page(pageManager, bufferForStringBuilder, read), lineBreaks)
+ : StringRebuilderForChars.Create(bufferForStringBuilder, read, lineBreaks);
+
+ content = content.Insert(content.Length, newContent);
+ }
+
+ longestLineLength = Math.Max(longestLineLength, currentLineLength);
+ hasConsistentLineEndings = lineEnding != LineEndingState.Inconsistent;
+ }
+ finally
+ {
+ if (!useCompressedStringRebuilders)
+ {
+ TextImageLoader.ReleaseBuffer(buffer);
+ }
+ }
+
+ return content;
+ }
+
+ public static int LoadNextBlock(TextReader reader, char[] buffer)
+ {
+ // Reserve 1 spot for a potential CR at the end of the buffer (in which we want to add the next LF, if it exists)
+ int read = reader.ReadBlock(buffer, 0, buffer.Length - 1);
+ if ((read == buffer.Length - 1) && (buffer[read - 1] == '\r'))
+ {
+ // Last character read was a CR and there is, probably since we read the entire block, more to go.
+ var next = reader.Peek();
+ if (next == '\n')
+ {
+ // We had a crlf that spanned the end of the buffer. Add it to the buffer and carry on.
+ // In theory we could append anything other than another CR but having the block end at
+ // the end of a line is a good thing.
+ reader.Read();
+ buffer[read++] = '\n';
+ }
+ }
+
+ return read;
+ }
+
+ private static void ParseBlock(char[] buffer, int length, ILineBreaksEditor lineBreaks,
+ ref LineEndingState lineEnding, ref int currentLineLength, ref int longestLineLength)
+ {
+ int index = 0;
+ while (index < length)
+ {
+ int breakLength = TextUtilities.LengthOfLineBreak(buffer, index, length);
+ if (breakLength == 0)
+ {
+ ++currentLineLength;
+ ++index;
+ }
+ else
+ {
+ lineBreaks.Add(index, breakLength);
+ longestLineLength = Math.Max(longestLineLength, currentLineLength);
+ currentLineLength = 0;
+
+ if (lineEnding != LineEndingState.Inconsistent)
+ {
+ if (breakLength == 2)
+ {
+ if (lineEnding == LineEndingState.Unknown)
+ lineEnding = LineEndingState.CRLF;
+ else if (lineEnding != LineEndingState.CRLF)
+ lineEnding = LineEndingState.Inconsistent;
+ }
+ else
+ {
+ LineEndingState newLineEndingState;
+ switch (buffer[index])
+ {
+ // This code needs to be kep consistent with TextUtilities.LengthOfLineBreak()
+ case '\r': newLineEndingState = LineEndingState.CR; break;
+ case '\n': newLineEndingState = LineEndingState.LF; break;
+ case '\u0085': newLineEndingState = LineEndingState.NEL; break;
+ case '\u2028': newLineEndingState = LineEndingState.LS; break;
+ case '\u2029': newLineEndingState = LineEndingState.PS; break;
+ default: throw new InvalidOperationException("Unexpected line ending");
+ }
+
+ if (lineEnding == LineEndingState.Unknown)
+ lineEnding = newLineEndingState;
+ else if (lineEnding != newLineEndingState)
+ lineEnding = LineEndingState.Inconsistent;
+ }
+ }
+
+ index += breakLength;
+ }
+ }
+ }
+
+ internal enum LineEndingState
+ {
+ Unknown = 0,
+ CRLF = 1,
+ CR = 2,
+ LF = 3,
+ NEL = 4, // unicode Next Line 0085
+ LS = 5, // unicode Line Separator 2028
+ PS = 6, // unicode Paragraph Separator 2029
+ Inconsistent = 7,
+ }
+
+ private static char[] pooledBuffer;
+
+ private static char[] AcquireBuffer(int size)
+ {
+ char[] buffer = Volatile.Read(ref pooledBuffer);
+ if (buffer != null && buffer.Length >= size)
+ {
+ if (buffer == Interlocked.CompareExchange(ref pooledBuffer, null, buffer))
+ {
+ return buffer;
+ }
+ }
+
+ return new char[size];
+ }
+
+ private static void ReleaseBuffer(char[] buffer)
+ {
+ Interlocked.CompareExchange(ref pooledBuffer, buffer, null);
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs
new file mode 100644
index 0000000..e3e3581
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs
@@ -0,0 +1,378 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+using System.Globalization;
+using System.Collections;
+using System.IO;
+using System.Threading;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal sealed class BinaryStringRebuilder : StringRebuilder
+ {
+ internal readonly StringRebuilder _left;
+ internal readonly StringRebuilder _right;
+
+#if DEBUG
+ private static int _totalCreated = 0;
+ public static int TotalCreated { get { return _totalCreated; } }
+#endif
+
+ #region Private
+ private static StringRebuilder _crlf = StringRebuilder.Create("\r\n");
+
+ // internal for unit tests, a \r\n can't be spanned by left and right
+ internal BinaryStringRebuilder(StringRebuilder left, StringRebuilder right)
+ : base(left.Length + right.Length, left.LineBreakCount + right.LineBreakCount, left.FirstCharacter, right.LastCharacter)
+ {
+ Debug.Assert(left.Length > 0);
+ Debug.Assert(right.Length > 0);
+ Debug.Assert(Math.Abs(left.Depth - right.Depth) <= 1);
+ Debug.Assert(left.LastCharacter != '\r' || right.FirstCharacter != '\n');
+
+#if DEBUG
+ Interlocked.Increment(ref _totalCreated);
+#endif
+
+ _left = left;
+ _right = right;
+ this.Depth = 1 + Math.Max(left.Depth, right.Depth);
+ }
+
+ private static StringRebuilder ConsolidateOrBalanceTreeNode(StringRebuilder left, StringRebuilder right)
+ {
+ if ((left.Length + right.Length < TextModelOptions.StringRebuilderMaxCharactersToConsolidate) &&
+ (left.LineBreakCount + right.LineBreakCount <= TextModelOptions.StringRebuilderMaxLinesToConsolidate))
+ {
+ //Consolidate the two rebuilders into a single simple string rebuilder
+ return StringRebuilder.Consolidate(left, right);
+ }
+ else
+ return BinaryStringRebuilder.BalanceTreeNode(left, right);
+ }
+
+ private static StringRebuilder BalanceStringRebuilder(StringRebuilder left, StringRebuilder right)
+ {
+ return BinaryStringRebuilder.BalanceTreeNode(left, right);
+ }
+
+ private static StringRebuilder BalanceTreeNode(StringRebuilder left, StringRebuilder right)
+ {
+ if (left.Depth > right.Depth + 1)
+ return BinaryStringRebuilder.Pivot(left, right, false);
+ else if (right.Depth > left.Depth + 1)
+ return BinaryStringRebuilder.Pivot(right, left, true);
+ else
+ return new BinaryStringRebuilder(left, right);
+ }
+
+ private static StringRebuilder Pivot(StringRebuilder child, StringRebuilder other, bool deepOnRightSide)
+ {
+ Debug.Assert(child.Depth > 0); //child's depth is greater than other's depth.
+ StringRebuilder grandchildOutside = child.Child(deepOnRightSide);
+ StringRebuilder grandchildInside = child.Child(!deepOnRightSide);
+
+ if (grandchildOutside.Depth >= grandchildInside.Depth)
+ {
+ //Simple pivot.
+ //From this (case deepOnRightSide)
+ // this
+ // / \
+ // other child
+ // ... / \
+ // gcI gcO
+ // ... ...
+ //
+ //To this:
+ // child'
+ // / \
+ // this' gcO
+ // / \ ...
+ // other gcI
+
+ StringRebuilder newThis;
+ StringRebuilder newChild;
+ if (deepOnRightSide)
+ {
+ newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(other, grandchildInside);
+ newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newThis, grandchildOutside);
+ }
+ else
+ {
+ newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildInside, other);
+ newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildOutside, newThis);
+ }
+
+ return newChild;
+ }
+ else
+ {
+ //Complex pivot.
+ //From this (case !deepOnRightSide)
+ // this
+ // / \
+ // other child
+ // ... / \
+ // gcI gcO
+ // / \ ...
+ // ggcI ggcO
+ // ... ...
+ //
+ //To this:
+ // gcI'
+ // / \
+ // this' child'
+ // / \ / \
+ // other ggcI ggcO gcO
+ // ... ... ... ...
+ Debug.Assert(grandchildInside.Depth > 0); //The inside's grandchild depth is > the outside grandchild's.
+ StringRebuilder greatgrandchildOutside = grandchildInside.Child(deepOnRightSide);
+ StringRebuilder greatgrandchildInside = grandchildInside.Child(!deepOnRightSide);
+
+ StringRebuilder newThis;
+ StringRebuilder newChild;
+ StringRebuilder newGcI;
+
+ if (deepOnRightSide)
+ {
+ newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(other, greatgrandchildInside);
+ newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(greatgrandchildOutside, grandchildOutside);
+ newGcI = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newThis, newChild);
+ }
+ else
+ {
+ newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(greatgrandchildInside, other);
+ newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildOutside, greatgrandchildOutside);
+ newGcI = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newChild, newThis);
+ }
+
+ return newGcI;
+ }
+ }
+ #endregion
+
+ public static StringRebuilder Create(StringRebuilder left, StringRebuilder right)
+ {
+ if (left == null)
+ throw new ArgumentNullException("left");
+ if (right == null)
+ throw new ArgumentNullException("right");
+
+ if (left.Length == 0)
+ return right;
+ else if (right.Length == 0)
+ return left;
+ else if ((left.Length + right.Length < TextModelOptions.StringRebuilderMaxCharactersToConsolidate) &&
+ (left.LineBreakCount + right.LineBreakCount <= TextModelOptions.StringRebuilderMaxLinesToConsolidate))
+ {
+ //Consolidate the two rebuilders into a single simple string rebuilder
+ return StringRebuilder.Consolidate(left, right);
+ }
+ else if ((right.FirstCharacter == '\n') && (left.LastCharacter == '\r'))
+ {
+ //Don't allow a line break to be broken across the seam
+ return BinaryStringRebuilder.Create(BinaryStringRebuilder.Create(left.GetSubText(new Span(0, left.Length - 1)),
+ _crlf),
+ right.GetSubText(Span.FromBounds(1, right.Length)));
+ }
+ else
+ {
+ return BinaryStringRebuilder.BalanceStringRebuilder(left, right);
+ }
+ }
+
+ public override string ToString()
+ {
+ return string.Format(System.Globalization.CultureInfo.InvariantCulture, this.Depth % 2 == 0 ? "({0})({1})" : "[{0}][{1}]",
+ _left.ToString(), _right.ToString());
+ }
+
+ public override int Depth { get; }
+
+ #region StringRebuilder Members
+ public override int GetLineNumberFromPosition(int position)
+ {
+ if ((position < 0) || (position > this.Length))
+ throw new ArgumentOutOfRangeException("position");
+
+ return (position <= _left.Length)
+ ? _left.GetLineNumberFromPosition(position)
+ : (_left.LineBreakCount +
+ _right.GetLineNumberFromPosition(position - _left.Length));
+ }
+
+ public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength)
+ {
+ if ((lineNumber < 0) || (lineNumber > this.LineBreakCount))
+ throw new ArgumentOutOfRangeException("lineNumber");
+
+ if (lineNumber < _left.LineBreakCount)
+ {
+ _left.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength);
+ }
+ else if (lineNumber > _left.LineBreakCount)
+ {
+ _right.GetLineFromLineNumber(lineNumber - _left.LineBreakCount, out extent, out lineBreakLength);
+ extent = new Span(extent.Start + _left.Length, extent.Length);
+ }
+ else
+ {
+ // The line crosses the seam.
+ int start = 0;
+ if (lineNumber != 0)
+ {
+ _left.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength); // ignore the returned extend.Length
+
+ start = extent.Start;
+ Debug.Assert(lineBreakLength == 0);
+ }
+
+ int end;
+
+ if (lineNumber == this.LineBreakCount)
+ {
+ end = this.Length;
+ lineBreakLength = 0;
+ }
+ else
+ {
+ _right.GetLineFromLineNumber(0, out extent, out lineBreakLength);
+ end = extent.End + _left.Length;
+ }
+
+ extent = Span.FromBounds(start, end);
+ }
+ }
+
+ public override StringRebuilder GetLeaf(int position, out int offset)
+ {
+ if (position < _left.Length)
+ {
+ return _left.GetLeaf(position, out offset);
+ }
+ else
+ {
+ var leaf = _right.GetLeaf(position - _left.Length, out offset);
+ offset += _left.Length;
+ return leaf;
+ }
+ }
+
+ public override char this[int index]
+ {
+ get
+ {
+ if ((index < 0) || (index >= this.Length))
+ throw new ArgumentOutOfRangeException("index");
+
+ return (index < _left.Length)
+ ? _left[index]
+ : _right[index - _left.Length];
+ }
+ }
+
+ public override string GetText(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.End <= _left.Length)
+ return _left.GetText(span);
+ else if (span.Start >= _left.Length)
+ return _right.GetText(new Span(span.Start - _left.Length, span.Length));
+ else
+ {
+ char[] result = new char[span.Length];
+
+ int leftLength = _left.Length - span.Start;
+ _left.CopyTo(span.Start, result, 0, leftLength);
+ _right.CopyTo(0, result, leftLength, span.Length - leftLength);
+
+ return new string(result);
+ }
+ }
+
+ public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ //These tests get executed a lot and are redundant: if there is an error, then the corresponding exception
+ //will be thrown when we reach a leaf node.
+
+ //if (sourceIndex < 0)
+ // throw new ArgumentOutOfRangeException("sourceIndex");
+ //if (destination == null)
+ // throw new ArgumentNullException("destination");
+ //if (destinationIndex < 0)
+ // throw new ArgumentOutOfRangeException("destinationIndex");
+ //if (count < 0)
+ // throw new ArgumentOutOfRangeException("count");
+
+ //if ((sourceIndex + count > this.Length) || (sourceIndex + count < 0))
+ // throw new ArgumentOutOfRangeException("count");
+
+ //if ((destinationIndex + count > destination.Length) || (destinationIndex + count < 0))
+ // throw new ArgumentOutOfRangeException("count");
+
+ if (sourceIndex >= _left.Length)
+ _right.CopyTo(sourceIndex - _left.Length, destination, destinationIndex, count);
+ else if (sourceIndex + count <= _left.Length)
+ _left.CopyTo(sourceIndex, destination, destinationIndex, count);
+ else
+ {
+ int leftLength = _left.Length - sourceIndex;
+
+ _left.CopyTo(sourceIndex, destination, destinationIndex, leftLength);
+ _right.CopyTo(0, destination, destinationIndex + leftLength, count - leftLength);
+ }
+ }
+
+ public override void Write(TextWriter writer, Span span)
+ {
+ if (writer == null)
+ throw new ArgumentNullException("writer");
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.Start >= _left.Length)
+ _right.Write(writer, new Span(span.Start - _left.Length, span.Length));
+ else if (span.End <= _left.Length)
+ _left.Write(writer, span);
+ else
+ {
+ _left.Write(writer, Span.FromBounds(span.Start, _left.Length));
+ _right.Write(writer, Span.FromBounds(0, span.End - _left.Length));
+ }
+ }
+
+ public override StringRebuilder GetSubText(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.Length == this.Length)
+ return this;
+ else if (span.End <= _left.Length)
+ return _left.GetSubText(span);
+ else if (span.Start >= _left.Length)
+ return _right.GetSubText(new Span(span.Start - _left.Length, span.Length));
+ else
+ return BinaryStringRebuilder.Create(_left.GetSubText(Span.FromBounds(span.Start, _left.Length)),
+ _right.GetSubText(Span.FromBounds(0, span.End - _left.Length)));
+ }
+
+ public override StringRebuilder Child(bool rightSide)
+ {
+ return rightSide ? _right : _left;
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs
new file mode 100644
index 0000000..1fe8b63
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs
@@ -0,0 +1,442 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ /// <summary>
+ /// An immutable variation on the StringBuilder class.
+ /// </summary>
+ internal abstract class StringRebuilder
+ {
+ public readonly static StringRebuilder Empty = new StringRebuilderForString();
+
+#if DEBUG
+ protected static int _totalCharactersScanned = 0;
+ public static int TotalCharactersScanned { get { return _totalCharactersScanned; } }
+
+ protected static int _totalCharactersReturned = 0;
+ public static int TotalCharactersReturned { get { return _totalCharactersReturned; } }
+
+ protected static int _totalCharactersCopied = 0;
+ public static int TotalCharactersCopied { get { return _totalCharactersCopied; } }
+#endif
+
+ public static StringRebuilder Create(string text)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+#if DEBUG
+ Interlocked.Add(ref _totalCharactersScanned, text.Length);
+#endif
+
+ return (text.Length == 0)
+ ? StringRebuilder.Empty
+ : StringRebuilderForString.Create(text, text.Length, LineBreakManager.CreateLineBreaks(text));
+ }
+
+ public static StringRebuilder Create(ITextImage image)
+ {
+ if (image == null)
+ throw new ArgumentNullException(nameof(image));
+
+ var cti = image as CachingTextImage;
+ if (cti != null)
+ return cti.Builder;
+
+ // This shouldn't happen but as a fallback, create a new string rebuilder from the text of the provided image.
+ return StringRebuilder.Create(image.GetText(0, image.Length));
+ }
+
+ /// <summary>
+ /// Consolidate two string rebuilders, taking advantage of the fact that they have already extracted the line breaks.
+ /// </summary>
+ public static StringRebuilder Consolidate(StringRebuilder left, StringRebuilder right)
+ {
+ Debug.Assert(left.Length > 0);
+ Debug.Assert(right.Length > 0);
+
+ int length = left.Length + right.Length;
+ char[] result = new char[length];
+
+ left.CopyTo(0, result, 0, left.Length);
+ right.CopyTo(0, result, left.Length, right.Length);
+
+ ILineBreaks lineBreaks;
+ if ((left.LineBreakCount == 0) && (right.LineBreakCount == 0))
+ {
+ lineBreaks = LineBreakManager.Empty;
+ //_lineBreakSpan defaults to 0, 0 which is what we want
+ }
+ else
+ {
+ ILineBreaksEditor breaks = LineBreakManager.CreateLineBreakEditor(length, left.LineBreakCount + right.LineBreakCount);
+
+ int offset = 0;
+ if ((result[left.Length] == '\n') && (result[left.Length - 1] == '\r'))
+ {
+ //We have a \r\n spanning the seam ... add that as a special linebreak later.
+ offset = 1;
+ }
+
+ int leftLines = left.LineBreakCount - offset;
+ for (int i = 0; (i < leftLines); ++i)
+ {
+ Span extent;
+ int lineBreakLength;
+ left.GetLineFromLineNumber(i, out extent, out lineBreakLength);
+ breaks.Add(extent.End, lineBreakLength);
+ }
+
+ if (offset == 1)
+ {
+ breaks.Add(left.Length - 1, 2);
+ }
+
+ for (int i = offset; (i < right.LineBreakCount); ++i)
+ {
+ Span extent;
+ int lineBreakLength;
+ right.GetLineFromLineNumber(i, out extent, out lineBreakLength);
+ breaks.Add(extent.End + left.Length, lineBreakLength);
+ }
+
+ lineBreaks = breaks;
+ }
+
+ return StringRebuilderForChars.Create(result, length, lineBreaks);
+ }
+
+ protected StringRebuilder(int length, int lineBreakCount, char first, char last)
+ {
+ this.Length = length;
+ this.LineBreakCount = lineBreakCount;
+ this.FirstCharacter = first;
+ this.LastCharacter = last;
+ }
+
+ /// <summary>
+ /// Number of characters in this <see cref="StringRebuilder"/>.
+ /// </summary>
+ public readonly int Length;
+
+ /// <summary>
+ /// Number of line breaks in this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <remarks>Line breaks consist of any of '\r', '\n', 0x85,
+ /// or a "\r\n" pair (which is treated as a single line break).</remarks>
+ public int LineBreakCount;
+
+ public virtual int Depth => 0;
+
+ /// <summary>
+ /// The first character of the StringRebuilder. \0 for a zero length StringRebuilder.
+ /// </summary>
+ public readonly char FirstCharacter;
+
+ /// <summary>
+ /// The last character of the StringRebuilder. \0 for a zero length StringRebuilder.
+ /// </summary>
+ public readonly char LastCharacter;
+
+#region Abstract methods
+ /// <summary>
+ /// Get the zero-based line number that contains <paramref name="position"/>.
+ /// </summary>
+ /// <param name="position">Position of the character for which to get the line number.</param>
+ /// <returns>Number of the line that contains <paramref name="position"/>.</returns>
+ /// <remarks>
+ /// Lines are bounded by line breaks and the start and end of this <see cref="StringRebuilder"/>.
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception>
+ public abstract int GetLineNumberFromPosition(int position);
+
+ /// <summary>
+ /// Get the TextImageLine associated with a zero-based line number.
+ /// </summary>
+ /// <param name="lineNumber">Line number for which to get the TextImageLine.</param>
+ /// <remarks>
+ /// <para>The last "line" in the StringRebuilder has an implicit line break length of zero.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero or greater than <see cref="LineBreakCount"/>.</exception>
+ public abstract void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength);
+
+ /// <summary>
+ /// Get the "leaf" node of the string rebuilder that contains position.
+ /// </summary>
+ /// <param name="position">position for which to get the leaf.</param>
+ /// <param name="offset">number of characters to the left of the leaf.</param>
+ /// <returns>leaf node from the string rebuilder.</returns>
+ public abstract StringRebuilder GetLeaf(int position, out int offset);
+
+ /// <summary>
+ /// Character at the given index.
+ /// </summary>
+ /// <param name="index">Index to get the character for.</param>
+ /// <returns>Character at position <paramref name="index"/>.</returns>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than zero or greater than or equal to <see cref="Length"/>.</exception>
+ public abstract char this[int index] { get; }
+
+ /// <summary>
+ /// Copy a range of text to a destination character array.
+ /// </summary>
+ /// <param name="sourceIndex">
+ /// The starting index to copy from.
+ /// </param>
+ /// <param name="destination">
+ /// The destination array.
+ /// </param>
+ /// <param name="destinationIndex">
+ /// The index in the destination of the first position to be copied to.
+ /// </param>
+ /// <param name="count">
+ /// The number of characters to copy.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="sourceIndex"/> is less than zero or greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is less than zero or <paramref name="sourceIndex"/> + <paramref name="count"/> is greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="destinationIndex"/> is less than zero or <paramref name="destinationIndex"/> + <paramref name="count"/> is greater than the length of <paramref name="destination"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception>
+ public abstract void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
+
+ /// <summary>
+ /// Write a substring of the contents of this <see cref="StringRebuilder"/> to a TextWriter.
+ /// </summary>
+ /// <param name="writer">TextWriter to use.</param>
+ /// <param name="span">Span to write.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ public abstract void Write(TextWriter writer, Span span);
+
+ /// <summary>
+ /// Create a new StringRebuilder that corresponds to a substring of this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="span">span that defines the desired substring.</param>
+ /// <returns>A new StringRebuilder containing the substring.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ public abstract StringRebuilder GetSubText(Span span);
+
+ /// <summary>
+ /// Get the string that contains all of the characters in the specified span.
+ /// </summary>
+ /// <param name="span">Span for which to get the text.</param>
+ /// <returns></returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> can contain millions of characters. Be careful what you
+ /// ask for: you might get it.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ public abstract string GetText(Span span);
+
+ public abstract StringRebuilder Child(bool rightSide);
+#endregion
+
+ /// <summary>
+ /// Convert a range of text to a character array.
+ /// </summary>
+ /// <param name="startIndex">
+ /// The starting index of the range of text.
+ /// </param>
+ /// <param name="length">
+ /// The length of the text.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="length"/> is less than zero or <paramref name="startIndex"/> + <paramref name="length"/> is greater than <see cref="Length"/>.</exception>
+ public char[] ToCharArray(int startIndex, int length)
+ {
+ if (startIndex < 0)
+ throw new ArgumentOutOfRangeException("startIndex");
+
+ if ((length < 0) || (startIndex + length > this.Length) || (startIndex + length < 0))
+ throw new ArgumentOutOfRangeException("length");
+
+ char[] copy = new char[length];
+ this.CopyTo(startIndex, copy, 0, length);
+
+ return copy;
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to appending text into this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="text">Text to append.</param>
+ /// <returns>A new StringRebuilder containing the insertion.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Append(string text)
+ {
+ return this.Insert(this.Length, text);
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to appending text into this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="text">Text to append.</param>
+ /// <returns>A new StringRebuilder containing the insertion.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Append(StringRebuilder text)
+ {
+ return this.Insert(this.Length, text);
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to inserting text into this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="position">Position at which to insert.</param>
+ /// <param name="text">Text to insert.</param>
+ /// <returns>A new StringRebuilder containing the insertion.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Insert(int position, string text)
+ {
+ return this.Insert(position, StringRebuilder.Create(text));
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to inserting text into this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="position">Position at which to insert.</param>
+ /// <param name="text">Text to insert.</param>
+ /// <returns>A new StringRebuilder containing the insertion.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Insert(int position, StringRebuilder text)
+ {
+ if ((position < 0) || (position > this.Length))
+ throw new ArgumentOutOfRangeException("position");
+ if (text == null)
+ throw new ArgumentNullException("text");
+
+ return this.Assemble(Span.FromBounds(0, position), text, Span.FromBounds(position, this.Length));
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to deleting text from this <see cref="StringRebuilder"/>.
+ /// </summary>
+ /// <param name="span">Span of text to delete.</param>
+ /// <returns>A new StringRebuilder containing the deletion.</returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ public StringRebuilder Delete(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ return this.Assemble(Span.FromBounds(0, span.Start), Span.FromBounds(span.End, this.Length));
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to replacing a contiguous span of characters
+ /// with different text.
+ /// </summary>
+ /// <param name="span">
+ /// Span of text in this <see cref="StringRebuilder"/> to replace.
+ /// </param>
+ /// <param name="text">
+ /// The new text to replace the old.
+ /// </param>
+ /// <returns>
+ /// A new string rebuilder containing the replacement.
+ /// </returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Replace(Span span, string text)
+ {
+ return this.Replace(span, StringRebuilder.Create(text));
+ }
+
+ /// <summary>
+ /// Create a new StringRebuilder equivalent to replacing a contiguous span of characters
+ /// with different text.
+ /// </summary>
+ /// <param name="span">
+ /// Span of text in this <see cref="StringRebuilder"/> to replace.
+ /// </param>
+ /// <param name="text">
+ /// The new text to replace the old.
+ /// </param>
+ /// <returns>
+ /// A new string rebuilder containing the replacement.
+ /// </returns>
+ /// <remarks>
+ /// <para>this <see cref="StringRebuilder"/> is not modified.</para>
+ /// <para>This operation can be performed simultaneously on multiple threads.</para>
+ /// </remarks>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception>
+ public StringRebuilder Replace(Span span, StringRebuilder text)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+ if (text == null)
+ throw new ArgumentNullException("text");
+
+ return this.Assemble(Span.FromBounds(0, span.Start), text, Span.FromBounds(span.End, this.Length));
+ }
+
+#region Private
+ private StringRebuilder Assemble(Span left, Span right)
+ {
+ if (left.Length == 0)
+ return this.GetSubText(right);
+ else if (right.Length == 0)
+ return this.GetSubText(left);
+ else if (left.Length + right.Length == this.Length)
+ return this;
+ else
+ return BinaryStringRebuilder.Create(this.GetSubText(left), this.GetSubText(right));
+ }
+
+ private StringRebuilder Assemble(Span left, StringRebuilder text, Span right)
+ {
+ if (text.Length == 0)
+ return Assemble(left, right);
+ else if (left.Length == 0)
+ return (right.Length == 0) ? text : BinaryStringRebuilder.Create(text, this.GetSubText(right));
+ else if (right.Length == 0)
+ return BinaryStringRebuilder.Create(this.GetSubText(left), text);
+ else if (left.Length < right.Length)
+ return BinaryStringRebuilder.Create(BinaryStringRebuilder.Create(this.GetSubText(left), text),
+ this.GetSubText(right));
+ else
+ return BinaryStringRebuilder.Create(this.GetSubText(left),
+ BinaryStringRebuilder.Create(text, this.GetSubText(right)));
+ }
+#endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs
new file mode 100644
index 0000000..a4f4293
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs
@@ -0,0 +1,90 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal class StringRebuilderForChars : UnaryStringRebuilder
+ {
+ #region Private
+ internal readonly char[] _content; // Contents should be treated as immutable.
+
+ internal static StringRebuilder Create(char[] source, int length, ILineBreaks lineBreaks)
+ {
+ return StringRebuilderForChars.Create(source, lineBreaks, 0, length, 0, lineBreaks.Length);
+ }
+
+ internal static StringRebuilder Create(char[] source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength)
+ {
+ Debug.Assert(length > 0);
+
+ if (lineBreaksLength == 0)
+ return new StringRebuilderForChars(source, LineBreakManager.Empty, start, length, 0, 0, source[start], source[start + length - 1]);
+ else
+ return new StringRebuilderForChars(source, lineBreaks, start, length, lineBreaksStart, lineBreaksLength, source[start], source[start + length - 1]);
+ }
+
+ private StringRebuilderForChars(char[] source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength, char first, char last)
+ : base(lineBreaks, start, length, lineBreaksStart, lineBreaksLength, first, last)
+ {
+ _content = source;
+ }
+ #endregion
+
+ public override string ToString()
+ {
+ return new string(_content, _textSpanStart, this.Length);
+ }
+
+ #region StringRebuilder Members
+
+ public override char this[int index]
+ {
+ get
+ {
+ return this.GetChar(_content, index);
+ }
+ }
+
+ public override string GetText(Span span)
+ {
+ return this.GetText(_content, span);
+ }
+
+ public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ this.CopyTo(_content, sourceIndex, destination, destinationIndex, count);
+ }
+
+ public override void Write(TextWriter writer, Span span)
+ {
+ this.Write(_content, writer, span);
+ }
+
+ public override StringRebuilder GetSubText(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.Length == 0)
+ return StringRebuilder.Empty;
+ else if (span.Length == this.Length)
+ return this;
+ else
+ {
+ int firstLineNumber;
+ int lastLineNumber;
+ this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber);
+ return StringRebuilderForChars.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber);
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs
new file mode 100644
index 0000000..ca80b09
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs
@@ -0,0 +1,82 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal class StringRebuilderForCompressedChars : UnaryStringRebuilder
+ {
+ private readonly Page _content;
+
+ internal static StringRebuilder Create(Page content, ILineBreaks lineBreaks)
+ {
+ return StringRebuilderForCompressedChars.Create(content, lineBreaks, 0, content.Length, 0, lineBreaks.Length);
+ }
+
+ private static StringRebuilder Create(Page content, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int linebreaksLength)
+ {
+ Debug.Assert(content.Length > 0);
+
+ var expanded = content.Expand();
+
+ return new StringRebuilderForCompressedChars(content, lineBreaks, start, length, lineBreaksStart, linebreaksLength, expanded[start], expanded[start + length - 1]);
+ }
+
+ private StringRebuilderForCompressedChars(Page content, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int linebreaksLength, char first, char last)
+ : base(lineBreaks, start, length, lineBreaksStart, linebreaksLength, first, last)
+ {
+ _content = content;
+ }
+
+ #region StringRebuilder Members
+ public override char this[int index]
+ {
+ get
+ {
+ return this.GetChar(_content.Expand(), index);
+ }
+ }
+
+ public override string GetText(Span span)
+ {
+ return this.GetText(_content.Expand(), span);
+ }
+
+ public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ this.CopyTo(_content.Expand(), sourceIndex, destination, destinationIndex, count);
+ }
+
+ public override void Write(TextWriter writer, Span span)
+ {
+ this.Write(_content.Expand(), writer, span);
+ }
+
+ public override StringRebuilder GetSubText(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.Length == this.Length)
+ return this;
+ else if (span.Length == 0)
+ return StringRebuilder.Empty;
+ else
+ {
+ int firstLineNumber;
+ int lastLineNumber;
+ this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber);
+
+ return StringRebuilderForCompressedChars.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber);
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs
new file mode 100644
index 0000000..56e5c8a
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs
@@ -0,0 +1,115 @@
+//
+// 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.
+//
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal class StringRebuilderForString : UnaryStringRebuilder
+ {
+ #region Private
+ internal readonly string _content;
+
+ internal static StringRebuilder Create(string source, int length, ILineBreaks lineBreaks)
+ {
+ return StringRebuilderForString.Create(source, lineBreaks, 0, length, 0, lineBreaks.Length);
+ }
+
+ internal static StringRebuilder Create(string source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength)
+ {
+ Debug.Assert(length != 0);
+
+ if (lineBreaksLength == 0)
+ return new StringRebuilderForString(source, LineBreakManager.Empty, start, length, 0, 0, source[start], source[start + length - 1]);
+ else
+ return new StringRebuilderForString(source, lineBreaks, start, length, lineBreaksStart, lineBreaksLength, source[start], source[start + length - 1]);
+ }
+
+ internal StringRebuilderForString()
+ : base(LineBreakManager.Empty, 0, 0, 0, 0, '\0', '\0')
+ {
+ _content = String.Empty;
+ }
+
+ private StringRebuilderForString(string source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength, char first, char last)
+ : base(lineBreaks, start, length, lineBreaksStart, lineBreaksLength, first, last)
+ {
+ _content = source;
+ }
+ #endregion
+
+ public override string ToString()
+ {
+ return _content.Substring(_textSpanStart, this.Length);
+ }
+
+ #region StringRebuilder Members
+
+ public override char this[int index]
+ {
+ get
+ {
+ if ((index < 0) || (index >= this.Length))
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ #if DEBUG
+ Interlocked.Increment(ref _totalCharactersReturned);
+ #endif
+
+ return _content[index + _textSpanStart];
+ }
+ }
+
+ public override string GetText(Span span)
+ {
+ #if DEBUG
+ Interlocked.Add(ref _totalCharactersReturned, span.Length);
+ #endif
+
+ return _content.Substring(span.Start + _textSpanStart, span.Length);
+ }
+
+ public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ #if DEBUG
+ Interlocked.Add(ref _totalCharactersCopied, count);
+ #endif
+
+ _content.CopyTo(sourceIndex + _textSpanStart, destination, destinationIndex, count);
+ }
+
+ public override void Write(TextWriter writer, Span span)
+ {
+ if (writer == null)
+ throw new ArgumentNullException(nameof(writer));
+
+ writer.Write(this.GetText(span));
+ }
+
+ public override StringRebuilder GetSubText(Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ if (span.Length == 0)
+ return StringRebuilder.Empty;
+ else if (span.Length == this.Length)
+ return this;
+ else
+ {
+ int firstLineNumber;
+ int lastLineNumber;
+ this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber);
+ return StringRebuilderForString.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber);
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs
new file mode 100644
index 0000000..3f53283
--- /dev/null
+++ b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs
@@ -0,0 +1,186 @@
+//
+// 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.
+//
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ internal abstract class UnaryStringRebuilder : StringRebuilder
+ {
+ internal readonly ILineBreaks _lineBreaks;
+
+ #if DEBUG
+ private static int _totalCreated = 0;
+ public static int TotalCreated { get { return _totalCreated; } }
+ #endif
+
+ protected readonly int _textSpanStart; //subspan of _storage contained in this StringRebuilderForChars
+ protected readonly int _lineBreakSpanStart; //subspan of _storage.LineBreaks that contains all line breaks in this StringRebuilderForChars
+ protected int TextSpanEnd { get { return _textSpanStart + this.Length; } }
+ protected int LineBreakSpanEnd { get { return _lineBreakSpanStart + this.LineBreakCount; } }
+
+ protected UnaryStringRebuilder(ILineBreaks lineBreaks, int start, int length, int linebreaksStart, int linebreaksLength, char first, char last)
+ : base(length, linebreaksLength, first, last)
+ {
+ #if DEBUG
+ Interlocked.Increment(ref _totalCreated);
+ #endif
+
+ _lineBreaks = lineBreaks;
+
+ _textSpanStart = start;
+ _lineBreakSpanStart = linebreaksStart;
+ }
+
+ internal void FindFirstAndLastLines(Span span, out int firstLineNumber, out int lastLineNumber)
+ {
+ firstLineNumber = this.GetLineNumberFromPosition(span.Start) + _lineBreakSpanStart;
+ lastLineNumber = this.GetLineNumberFromPosition(span.End) + _lineBreakSpanStart;
+
+ //Handle the special case where the end position falls in the middle of a linebreak.
+ if ((lastLineNumber < this.LineBreakSpanEnd) &&
+ (span.End > _lineBreaks.StartOfLineBreak(lastLineNumber) - _textSpanStart))
+ {
+ ++lastLineNumber;
+ }
+ }
+
+ #region StringRebuilder Members
+ public override int GetLineNumberFromPosition(int position)
+ {
+ if ((position < 0) || (position > this.Length))
+ throw new ArgumentOutOfRangeException("position");
+
+ //Convert position to a position relative to the start of _text.
+ if (position == this.Length)
+ {
+ //Handle positions at the end of the span as a special case since otherwise we
+ //return the incorrect value if the last line break extends past the end of _textSpan.
+ return this.LineBreakCount;
+ }
+
+ position += _textSpanStart;
+
+ int start = _lineBreakSpanStart;
+ int end = this.LineBreakSpanEnd;
+
+ while (start < end)
+ {
+ int middle = (start + end) / 2;
+ if (position < _lineBreaks.EndOfLineBreak(middle))
+ end = middle;
+ else
+ start = middle + 1;
+ }
+
+ return start - _lineBreakSpanStart;
+ }
+
+ public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength)
+ {
+ if ((lineNumber < 0) || (lineNumber > this.LineBreakCount))
+ throw new ArgumentOutOfRangeException("lineNumber");
+
+ int absoluteLineNumber = _lineBreakSpanStart + lineNumber;
+
+ int start = (lineNumber == 0)
+ ? 0
+ : (Math.Min(this.TextSpanEnd, _lineBreaks.EndOfLineBreak(absoluteLineNumber - 1)) - _textSpanStart);
+
+ int end;
+ if (lineNumber < this.LineBreakCount)
+ {
+ end = Math.Max(_textSpanStart, _lineBreaks.StartOfLineBreak(absoluteLineNumber));
+ lineBreakLength = Math.Min(this.TextSpanEnd, _lineBreaks.EndOfLineBreak(absoluteLineNumber)) - end;
+
+ end -= _textSpanStart;
+ }
+ else
+ {
+ end = this.Length;
+ lineBreakLength = 0;
+ }
+
+ extent = Span.FromBounds(start, end);
+
+ }
+
+ public override StringRebuilder GetLeaf(int position, out int offset)
+ {
+ offset = 0;
+ return this;
+ }
+
+ protected char GetChar(char[] content, int index)
+ {
+ if ((index < 0) || (index >= this.Length))
+ throw new ArgumentOutOfRangeException("index");
+
+ #if DEBUG
+ Interlocked.Increment(ref _totalCharactersReturned);
+ #endif
+
+ return content[index + _textSpanStart];
+ }
+
+ protected string GetText(char[] content, Span span)
+ {
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ #if DEBUG
+ Interlocked.Add(ref _totalCharactersReturned, span.Length);
+ #endif
+
+ return new string(content, span.Start + _textSpanStart, span.Length);
+ }
+
+ protected void CopyTo(char[] content, int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ if (sourceIndex < 0)
+ throw new ArgumentOutOfRangeException("sourceIndex");
+ if (destination == null)
+ throw new ArgumentNullException("destination");
+ if (destinationIndex < 0)
+ throw new ArgumentOutOfRangeException("destinationIndex");
+ if (count < 0)
+ throw new ArgumentOutOfRangeException("count");
+
+ if ((sourceIndex + count > this.Length) || (sourceIndex + count < 0))
+ throw new ArgumentOutOfRangeException("count");
+
+ if ((destinationIndex + count > destination.Length) || (destinationIndex + count < 0))
+ throw new ArgumentOutOfRangeException("count");
+
+ #if DEBUG
+ Interlocked.Add(ref _totalCharactersCopied, count);
+ #endif
+
+ Array.Copy(content, sourceIndex + _textSpanStart, destination, destinationIndex, count);
+ }
+
+ protected void Write(char[] content, TextWriter writer, Span span)
+ {
+ if (writer == null)
+ throw new ArgumentNullException("writer");
+ if (span.End > this.Length)
+ throw new ArgumentOutOfRangeException("span");
+
+ writer.Write(content, span.Start + _textSpanStart, span.Length);
+ }
+
+ public override StringRebuilder Child(bool rightSide)
+ {
+ throw new InvalidOperationException();
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/Strings.Designer.cs b/src/Text/Impl/TextModel/Strings.Designer.cs
new file mode 100644
index 0000000..47ec6fd
--- /dev/null
+++ b/src/Text/Impl/TextModel/Strings.Designer.cs
@@ -0,0 +1,270 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17462
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.VisualStudio.Text.Implementation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Strings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Strings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Strings", typeof(Strings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to cancel an applied edit..
+ /// </summary>
+ internal static string CancelAppliedEdit {
+ get {
+ return ResourceManager.GetString("CancelAppliedEdit", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to continue a canceled edit..
+ /// </summary>
+ internal static string ContinueCanceledEdit {
+ get {
+ return ResourceManager.GetString("ContinueCanceledEdit", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to File is too large to open..
+ /// </summary>
+ internal static string FileTooLarge {
+ get {
+ return ResourceManager.GetString("FileTooLarge", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to change edit thread of TextBuffer..
+ /// </summary>
+ internal static string InvalidBufferThreadOwnershipChange {
+ get {
+ return ResourceManager.GetString("InvalidBufferThreadOwnershipChange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Edge inclusive projection source span must cover entire source TextBuffer..
+ /// </summary>
+ internal static string InvalidEdgeInclusiveSourceSpan {
+ get {
+ return ResourceManager.GetString("InvalidEdgeInclusiveSourceSpan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to EdgeNegative source span must abut start of source TextBuffer..
+ /// </summary>
+ internal static string InvalidEdgeNegativeSourceSpan {
+ get {
+ return ResourceManager.GetString("InvalidEdgeNegativeSourceSpan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to EdgePositive source span must abut end of source TextBuffer..
+ /// </summary>
+ internal static string InvalidEdgePositiveSourceSpan {
+ get {
+ return ResourceManager.GetString("InvalidEdgePositiveSourceSpan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Inconsistent length calculation in projection snapshot..
+ /// </summary>
+ internal static string InvalidLengthCalculation {
+ get {
+ return ResourceManager.GetString("InvalidLengthCalculation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Inconsistent line count calculation in projection snapshot..
+ /// </summary>
+ internal static string InvalidLineCountCalculation {
+ get {
+ return ResourceManager.GetString("InvalidLineCountCalculation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified IReadOnlyRegion doesn&apos;t belong to the correct TextBuffer..
+ /// </summary>
+ internal static string InvalidReadOnlyRegion {
+ get {
+ return ResourceManager.GetString("InvalidReadOnlyRegion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to StringRebuilder enumerator is not at a valid position..
+ /// </summary>
+ internal static string InvalidRebuilderEnumerator {
+ get {
+ return ResourceManager.GetString("InvalidRebuilderEnumerator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified ITextSnapshot doesn&apos;t belong to the correct TextBuffer..
+ /// </summary>
+ internal static string InvalidSnapshot {
+ get {
+ return ResourceManager.GetString("InvalidSnapshot", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to map to invalid target TextBuffer..
+ /// </summary>
+ internal static string InvalidTargetBuffer {
+ get {
+ return ResourceManager.GetString("InvalidTargetBuffer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to edit TextBuffer on the wrong thread..
+ /// </summary>
+ internal static string InvalidTextBufferEditThread {
+ get {
+ return ResourceManager.GetString("InvalidTextBufferEditThread", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified ITrackingSpan doesn&apos;t belong to the correct TextBuffer..
+ /// </summary>
+ internal static string InvalidTrackingSpan {
+ get {
+ return ResourceManager.GetString("InvalidTrackingSpan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified TextVersion doesn&apos;t belong to the correct TextBuffer..
+ /// </summary>
+ internal static string InvalidVersion {
+ get {
+ return ResourceManager.GetString("InvalidVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot map down except from the top buffer..
+ /// </summary>
+ internal static string MapDownFromNonTopBuffer {
+ get {
+ return ResourceManager.GetString("MapDownFromNonTopBuffer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Source Span is neither an ITrackingSpan nor a String literal.
+ /// </summary>
+ internal static string NeitherSpanNorString {
+ get {
+ return ResourceManager.GetString("NeitherSpanNorString", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to introduce overlapping source spans..
+ /// </summary>
+ internal static string OverlappingSourceSpans {
+ get {
+ return ResourceManager.GetString("OverlappingSourceSpans", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There are no read only regions to remove..
+ /// </summary>
+ internal static string RemoveNoReadOnlyRegion {
+ get {
+ return ResourceManager.GetString("RemoveNoReadOnlyRegion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to reuse an already applied edit..
+ /// </summary>
+ internal static string ReuseAppliedEdit {
+ get {
+ return ResourceManager.GetString("ReuseAppliedEdit", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted TextBuffer edit operation while another edit is in progress..
+ /// </summary>
+ internal static string SimultaneousEdit {
+ get {
+ return ResourceManager.GetString("SimultaneousEdit", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempted to create cyclic ProjectionBuffer..
+ /// </summary>
+ internal static string SourceBufferCycle {
+ get {
+ return ResourceManager.GetString("SourceBufferCycle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/Strings.resx b/src/Text/Impl/TextModel/Strings.resx
new file mode 100644
index 0000000..c911643
--- /dev/null
+++ b/src/Text/Impl/TextModel/Strings.resx
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="InvalidSnapshot" xml:space="preserve">
+ <value>The specified ITextSnapshot doesn't belong to the correct TextBuffer.</value>
+ </data>
+ <data name="InvalidVersion" xml:space="preserve">
+ <value>The specified TextVersion doesn't belong to the correct TextBuffer.</value>
+ </data>
+ <data name="InvalidReadOnlyRegion" xml:space="preserve">
+ <value>The specified IReadOnlyRegion doesn't belong to the correct TextBuffer.</value>
+ </data>
+ <data name="InvalidTrackingSpan" xml:space="preserve">
+ <value>The specified ITrackingSpan doesn't belong to the correct TextBuffer.</value>
+ </data>
+ <data name="RemoveNoReadOnlyRegion" xml:space="preserve">
+ <value>There are no read only regions to remove.</value>
+ </data>
+ <data name="OverlappingSourceSpans" xml:space="preserve">
+ <value>Attempted to introduce overlapping source spans.</value>
+ </data>
+ <data name="SimultaneousEdit" xml:space="preserve">
+ <value>Attempted TextBuffer edit operation while another edit is in progress.</value>
+ </data>
+ <data name="CancelAppliedEdit" xml:space="preserve">
+ <value>Attempted to cancel an applied edit.</value>
+ </data>
+ <data name="ReuseAppliedEdit" xml:space="preserve">
+ <value>Attempted to reuse an already applied edit.</value>
+ </data>
+ <data name="ContinueCanceledEdit" xml:space="preserve">
+ <value>Attempted to continue a canceled edit.</value>
+ </data>
+ <data name="InvalidRebuilderEnumerator" xml:space="preserve">
+ <value>StringRebuilder enumerator is not at a valid position.</value>
+ </data>
+ <data name="InvalidEdgeInclusiveSourceSpan" xml:space="preserve">
+ <value>Edge inclusive projection source span must cover entire source TextBuffer.</value>
+ </data>
+ <data name="InvalidBufferThreadOwnershipChange" xml:space="preserve">
+ <value>Attempted to change edit thread of TextBuffer.</value>
+ </data>
+ <data name="InvalidTextBufferEditThread" xml:space="preserve">
+ <value>Attempted to edit TextBuffer on the wrong thread.</value>
+ </data>
+ <data name="InvalidEdgeNegativeSourceSpan" xml:space="preserve">
+ <value>EdgeNegative source span must abut start of source TextBuffer.</value>
+ </data>
+ <data name="InvalidEdgePositiveSourceSpan" xml:space="preserve">
+ <value>EdgePositive source span must abut end of source TextBuffer.</value>
+ </data>
+ <data name="NeitherSpanNorString" xml:space="preserve">
+ <value>Source Span is neither an ITrackingSpan nor a String literal</value>
+ </data>
+ <data name="SourceBufferCycle" xml:space="preserve">
+ <value>Attempted to create cyclic ProjectionBuffer.</value>
+ </data>
+ <data name="InvalidLengthCalculation" xml:space="preserve">
+ <value>Inconsistent length calculation in projection snapshot.</value>
+ </data>
+ <data name="InvalidTargetBuffer" xml:space="preserve">
+ <value>Attempt to map to invalid target TextBuffer.</value>
+ </data>
+ <data name="MapDownFromNonTopBuffer" xml:space="preserve">
+ <value>Cannot map down except from the top buffer.</value>
+ </data>
+ <data name="InvalidLineCountCalculation" xml:space="preserve">
+ <value>Inconsistent line count calculation in projection snapshot.</value>
+ </data>
+ <data name="FileTooLarge" xml:space="preserve">
+ <value>File is too large to open.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/TextBuffer.cs b/src/Text/Impl/TextModel/TextBuffer.cs
new file mode 100644
index 0000000..b6023fa
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextBuffer.cs
@@ -0,0 +1,287 @@
+//
+// 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.Collections.Generic;
+ using System.Diagnostics;
+
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal sealed partial class TextBuffer : BaseBuffer
+ {
+ #region BasicEdit class
+ private class BasicEdit : Edit, ISubordinateTextEdit
+ {
+ private TextBuffer textBuffer;
+ private bool subordinate;
+
+ public BasicEdit(TextBuffer textBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag)
+ : base(textBuffer, originSnapshot, options, reiteratedVersionNumber, editTag)
+ {
+ this.textBuffer = textBuffer;
+ this.subordinate = true;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.textBuffer; }
+ }
+
+ // this is the master edit path -- initiated from outside
+ protected override ITextSnapshot PerformApply()
+ {
+ CheckActive();
+ this.applied = true;
+ this.subordinate = false;
+
+ ITextSnapshot result = this.textBuffer.CurrentSnapshot;
+
+ if (this.changes.Count > 0)
+ {
+ if (this.textBuffer.group.Members.Count == 1 || this.textBuffer.spurnGroup)
+ {
+ // Take a simpler faster path if there are no other buffers in our group
+ if (this.CheckForCancellation(() => { }))
+ {
+ FinalApply();
+ result = this.textBuffer.CurrentSnapshot;
+ }
+ else
+ {
+ Debug.Assert(this.canceled);
+ Debug.Assert(!this.baseBuffer.editInProgress);
+ }
+ }
+ else
+ {
+ this.textBuffer.group.PerformMasterEdit(this.textBuffer, this, this.options, this.editTag);
+
+ if (!this.Canceled)
+ {
+ result = this.textBuffer.CurrentSnapshot;
+ }
+ }
+ }
+ else
+ {
+ // vacuous edit
+ this.baseBuffer.editInProgress = false;
+ }
+
+ Debug.Assert(!this.baseBuffer.editInProgress);
+
+ return result;
+ }
+
+ public void PreApply()
+ {
+ // called for all non-vacuous edits
+ // everything happens in FinalApply()
+ }
+
+ public void FinalApply()
+ {
+ Debug.Assert(!this.canceled);
+ if (this.changes.Count > 0)
+ {
+ ITextEventRaiser eventRaiser = this.textBuffer.ApplyChangesAndSetSnapshot(this.changes, this.options, this.reiteratedVersionNumber, this.editTag);
+ this.baseBuffer.group.EnqueueEvents(eventRaiser, this.baseBuffer);
+
+ // raise immediate events
+ eventRaiser.RaiseEvent(this.baseBuffer, true);
+ }
+
+ this.baseBuffer.editInProgress = false;
+ if (this.subordinate)
+ {
+ this.baseBuffer.group.FinishEdit();
+ }
+ }
+ }
+ #endregion
+
+ #region ReloadEdit class
+ private class ReloadEdit : TextBufferBaseEdit, ISubordinateTextEdit
+ {
+ private StringRebuilder newContent;
+ private TextBuffer textBuffer;
+ private ITextSnapshot originSnapshot;
+ private object editTag;
+ private EditOptions editOptions;
+ private TextContentChangingEventArgs raisedChangingEventArgs;
+ private Action cancelAction;
+
+ public ReloadEdit(TextBuffer textBuffer, ITextSnapshot originSnapshot, EditOptions editOptions, object editTag) : base(textBuffer)
+ {
+ this.textBuffer = textBuffer;
+ this.originSnapshot = originSnapshot;
+ this.editOptions = editOptions;
+ this.editTag = editTag;
+ }
+
+ public ITextSnapshot ReloadContent(StringRebuilder newContent)
+ {
+ if (this.baseBuffer.IsReadOnlyImplementation(new Span(0, this.originSnapshot.Length), isEdit: true))
+ {
+ this.applied = true;
+ this.baseBuffer.editInProgress = false;
+ this.baseBuffer.group.FinishEdit();
+ return this.originSnapshot;
+ }
+ else
+ {
+ this.newContent = newContent;
+ this.baseBuffer.group.PerformMasterEdit(this.textBuffer, this, this.editOptions, this.editTag);
+ this.baseBuffer.group.FinishEdit();
+ return this.textBuffer.CurrentSnapshot;
+ }
+ }
+
+ public void PreApply()
+ {
+ }
+
+ // copied from BaseBuffer.Edit. Could arrange to inherit.
+ public bool CheckForCancellation(Action cancelationResponse)
+ {
+ Debug.Assert(this.raisedChangingEventArgs == null, "just checking");
+ if (this.raisedChangingEventArgs == null)
+ {
+ this.cancelAction = cancelationResponse;
+ this.raisedChangingEventArgs = new TextContentChangingEventArgs(this.originSnapshot, this.editTag, (args) =>
+ {
+ this.Cancel();
+ });
+ this.baseBuffer.RaiseChangingEvent(this.raisedChangingEventArgs);
+ }
+ this.canceled = this.raisedChangingEventArgs.Canceled;
+ return !this.raisedChangingEventArgs.Canceled;
+ }
+
+ public void FinalApply()
+ {
+ TextContentChangedEventArgs args = this.textBuffer.ApplyReload(this.newContent, this.editOptions, this.editTag);
+ TextContentChangedEventRaiser raiser = new TextContentChangedEventRaiser(this.originSnapshot, this.baseBuffer.currentSnapshot, this.editOptions, this.editTag);
+ this.applied = true;
+ this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer);
+ raiser.RaiseEvent(this.baseBuffer, true);
+ this.baseBuffer.editInProgress = false;
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get { return this.baseBuffer; }
+ }
+
+ public void RecordMasterChangeOffset(int masterChangeOffset)
+ {
+ throw new InvalidOperationException("Reloads should not be getting offsets from any other change.");
+ }
+ }
+ #endregion
+
+ #region State and Construction
+ bool spurnGroup;
+
+ public TextBuffer(IContentType contentType, StringRebuilder content, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations)
+ : this(contentType, content, textDifferencingService, guardedOperations, false)
+ {
+ }
+
+ public TextBuffer(IContentType contentType, StringRebuilder content, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations, bool spurnGroup)
+ : base(contentType, content.Length, textDifferencingService, guardedOperations)
+ {
+ // Parameters are validated outside
+ this.group = new BufferGroup(this);
+ this.builder = content;
+ this.spurnGroup = spurnGroup;
+ this.currentSnapshot = this.TakeSnapshot();
+ }
+ #endregion
+
+ #region Reload
+ /// <summary>
+ /// Replace the contents of the buffer with the contents of a different string rebuilder.
+ /// </summary>
+ /// <param name="newContent">The new contents of the buffer (presumably read from a file).</param>
+ /// <param name="editOptions">Options to apply to the edit. Differencing is highly likely to be selected.</param>
+ /// <param name="editTag">Arbitrary tag associated with the reload that will appear in event arguments.</param>
+ /// <returns></returns>
+ public ITextSnapshot ReloadContent(StringRebuilder newContent, EditOptions editOptions, object editTag)
+ {
+ using (ReloadEdit edit = new ReloadEdit(this, this.currentSnapshot, editOptions, editTag))
+ {
+ return edit.ReloadContent(newContent);
+ }
+ }
+
+ internal TextContentChangedEventArgs ApplyReload(StringRebuilder newContent, EditOptions editOptions, object editTag)
+ {
+ // we construct a normalized change list where the inserted text is a reference string that
+ // points "forward" to the next snapshot and whose deleted text is a reference string that points
+ // "backward" to the prior snapshot. This pins both snapshots in memory but that's better than materializing
+ // giant strings, and when (?) we have paging text storage, memory requirements will be minimal.
+ ITextSnapshot oldSnapshot = this.currentSnapshot;
+ StringRebuilder oldContent = BufferFactoryService.StringRebuilderFromSnapshotSpan(new SnapshotSpan(oldSnapshot, 0, oldSnapshot.Length));
+
+ TextChange change = TextChange.Create(oldPosition: 0,
+ oldText: oldContent,
+ newText: newContent,
+ currentSnapshot: oldSnapshot);
+
+
+ TextVersion newVersion = this.currentVersion.CreateNext(changes: null, newLength: newContent.Length, reiteratedVersionNumber: -1);
+ TextSnapshot newSnapshot = new TextSnapshot(this, newVersion, newContent);
+
+ this.currentVersion.SetChanges(NormalizedTextChangeCollection.Create(new TextChange[] { change },
+ editOptions.ComputeMinimalChange
+ ? (StringDifferenceOptions?)editOptions.DifferenceOptions
+ : null,
+ this.textDifferencingService,
+ oldSnapshot, newSnapshot));
+
+
+ this.currentVersion = newVersion;
+ this.builder = newContent;
+ this.currentSnapshot = newSnapshot;
+ return new TextContentChangedEventArgs(oldSnapshot, newSnapshot, editOptions, editTag);
+ }
+ #endregion
+
+ #region Overridden methods
+ public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new BasicEdit(this, this.currentSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+
+ protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ return new BasicEdit(this, this.currentSnapshot, options, reiteratedVersionNumber, editTag);
+ }
+
+ private ITextEventRaiser ApplyChangesAndSetSnapshot(FrugalList<TextChange> changes, EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(changes,
+ options.ComputeMinimalChange ? (StringDifferenceOptions?)options.DifferenceOptions : null,
+ this.textDifferencingService);
+ ITextSnapshot originSnapshot = base.CurrentSnapshot;
+ base.SetCurrentVersionAndSnapshot(normalizedChanges, reiteratedVersionNumber ?? -1);
+
+ return new TextContentChangedEventRaiser(originSnapshot, this.CurrentSnapshot, options, editTag);
+ }
+
+ protected override BaseSnapshot TakeSnapshot()
+ {
+ return new TextSnapshot(this, this.currentVersion, this.builder);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextChange.cs b/src/Text/Impl/TextModel/TextChange.cs
new file mode 100644
index 0000000..972854e
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextChange.cs
@@ -0,0 +1,351 @@
+//
+// 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 Microsoft.VisualStudio.Text.Utilities;
+
+ /// <summary>
+ /// Describes a single contiguous atomic text change operation on the Text Buffer.
+ ///
+ /// All text changes are modeled as the replacement of parameter oldText with parameter newText
+ ///
+ /// Insertion: oldText == "" and newText != ""
+ /// Deletion: oldText != "" and newText == ""
+ /// Replace: oldText != "" and newText != ""
+ /// </summary>
+ internal partial class TextChange : ITextChange3
+ {
+ #region Private Members
+
+ private int _oldPosition;
+ private int _newPosition;
+ internal StringRebuilder _oldText, _newText;
+ private LineBreakBoundaryConditions _lineBreakBoundaryConditions;
+ private bool _isOpaque;
+
+ private int? _lineCountDelta = null;
+ private int _masterChangeOffset = -1;
+
+ #endregion // Private Members
+
+ /// <summary>
+ /// Constructs a Text Change object.
+ /// </summary>
+ /// <param name="oldPosition">
+ /// The character position in the TextBuffer at which the text change happened.
+ /// </param>
+ /// <param name="oldText">
+ /// The text in the buffer that was replaced.
+ /// </param>
+ /// <param name="newText">
+ /// The text that replaces the old text.
+ /// </param>
+ /// <param name="boundaryConditions">
+ /// Information about neighboring line break characters.
+ /// </param>
+ public TextChange(int oldPosition, StringRebuilder oldText, StringRebuilder newText, LineBreakBoundaryConditions boundaryConditions)
+ {
+ if (oldPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException("oldPosition");
+ }
+
+ _oldPosition = oldPosition;
+ _newPosition = oldPosition;
+ _oldText = oldText;
+ _newText = newText;
+ _lineBreakBoundaryConditions = boundaryConditions;
+ }
+
+ internal TextChange(int oldPosition, string oldText, string newText, LineBreakBoundaryConditions boundaryConditions)
+ : this(oldPosition, StringRebuilder.Create(oldText), StringRebuilder.Create(newText), boundaryConditions)
+ { }
+
+ public static TextChange Create(int oldPosition, string oldText, string newText, ITextSnapshot currentSnapshot)
+ {
+ return new TextChange(oldPosition, StringRebuilder.Create(oldText), StringRebuilder.Create(newText), ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length));
+ }
+
+ public static TextChange Create(int oldPosition, StringRebuilder oldText, string newText, ITextSnapshot currentSnapshot)
+ {
+ return new TextChange(oldPosition, oldText, StringRebuilder.Create(newText), ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length));
+ }
+
+ public static TextChange Create(int oldPosition, string oldText, StringRebuilder newText, ITextSnapshot currentSnapshot)
+ {
+ return new TextChange(oldPosition, StringRebuilder.Create(oldText), newText, ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length));
+ }
+
+ public static TextChange Create(int oldPosition, StringRebuilder oldText, StringRebuilder newText, ITextSnapshot currentSnapshot)
+ {
+ return new TextChange(oldPosition, oldText, newText, ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length));
+ }
+
+ #region Public Properties
+
+ public Span OldSpan
+ {
+ get { return new Span(_oldPosition, _oldText.Length); }
+ }
+
+ public Span NewSpan
+ {
+ get { return new Span(_newPosition, _newText.Length); }
+ }
+
+ public int OldPosition
+ {
+ get { return _oldPosition; }
+ internal set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value");
+ }
+ _oldPosition = value;
+ }
+ }
+
+ public int NewPosition
+ {
+ get { return _newPosition; }
+ internal set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value");
+ }
+ _newPosition = value;
+ }
+ }
+
+ public int Delta
+ {
+ get { return _newText.Length - _oldText.Length; }
+ }
+
+ public int OldEnd
+ {
+ get { return _oldPosition + _oldText.Length; }
+ }
+
+ public int NewEnd
+ {
+ get { return _newPosition + _newText.Length; }
+ }
+
+ public string OldText
+ {
+ get { return _oldText.GetText(new Span(0, _oldText.Length)); }
+ }
+
+ public string NewText
+ {
+ get { return _newText.GetText(new Span(0, _newText.Length)); }
+ }
+
+ public int NewLength
+ {
+ get { return _newText.Length; }
+ }
+
+ public int OldLength
+ {
+ get { return _oldText.Length; }
+ }
+
+ public int LineCountDelta
+ {
+ get
+ {
+ // we are lazy
+ if (!_lineCountDelta.HasValue)
+ {
+ _lineCountDelta = TextModelUtilities.ComputeLineCountDelta(_lineBreakBoundaryConditions, _oldText, _newText);
+ }
+ return _lineCountDelta.Value;
+ }
+ }
+
+ public bool IsOpaque
+ {
+ get { return _isOpaque; }
+ internal set { _isOpaque = value; }
+ }
+ #endregion // Public Properties
+
+ #region Public Methods
+ public string GetOldText(Span span)
+ {
+ return _oldText.GetText(span);
+ }
+
+ public string GetNewText(Span span)
+ {
+ return _newText.GetText(span);
+ }
+
+ public char GetOldTextAt(int position)
+ {
+ if (position > this.OldLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(position));
+ }
+
+ return _oldText[position];
+ }
+
+ public char GetNewTextAt(int position)
+ {
+ if (position > this.NewLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(position));
+ }
+
+ return _newText[position];
+ }
+ #endregion
+
+ #region Internal Properties
+ internal LineBreakBoundaryConditions LineBreakBoundaryConditions
+ {
+ get { return _lineBreakBoundaryConditions; }
+ set
+ {
+ _lineBreakBoundaryConditions = value;
+ _lineCountDelta = null;
+ }
+ }
+
+ internal void RecordMasterChangeOffset(int masterChangeOffset)
+ {
+ if (masterChangeOffset < 0)
+ throw new ArgumentOutOfRangeException("masterChangeOffset", "MasterChangeOffset should be non-negative.");
+ if (_masterChangeOffset != -1)
+ throw new InvalidOperationException("MasterChangeOffset has already been set.");
+
+ _masterChangeOffset = masterChangeOffset;
+ }
+
+ internal int MasterChangeOffset { get { return _masterChangeOffset == -1 ? 0 : _masterChangeOffset; } }
+
+ internal static int Compare(TextChange x, TextChange y)
+ {
+ int diff = x.OldPosition - y.OldPosition;
+ if (diff != 0)
+ return diff;
+ else
+ return x.MasterChangeOffset - y.MasterChangeOffset;
+ }
+
+ #endregion
+
+ #region Private helper
+ private static LineBreakBoundaryConditions ComputeLineBreakBoundaryConditions(ITextSnapshot currentSnapshot, int position, int oldLength)
+ {
+ LineBreakBoundaryConditions conditions = LineBreakBoundaryConditions.None;
+ if (position > 0 && currentSnapshot[position - 1] == '\r')
+ {
+ conditions = LineBreakBoundaryConditions.PrecedingReturn;
+ }
+ int end = position + oldLength;
+ if (end < currentSnapshot.Length && currentSnapshot[end] == '\n')
+ {
+ conditions = conditions | LineBreakBoundaryConditions.SucceedingNewline;
+ }
+ return conditions;
+ }
+ #endregion
+
+ #region Overridden methods
+ public string ToString(bool brief)
+ {
+ if (brief)
+ {
+ return string.Format(System.Globalization.CultureInfo.InvariantCulture, "old={0} new={1}", this.OldSpan, this.NewSpan);
+ }
+ else
+ {
+ return string.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "old={0}:'{1}' new={2}:'{3}'",
+ this.OldSpan, TextUtilities.Escape(this.OldText), this.NewSpan, TextUtilities.Escape(this.NewText, 40));
+ }
+ }
+
+ public override string ToString()
+ {
+ return ToString(false);
+ }
+ #endregion
+
+ public static StringRebuilder OldStringRebuilder(ITextChange change)
+ {
+ var textChange = change as TextChange;
+ return (textChange != null) ? textChange._oldText : StringRebuilder.Create(change.OldText);
+ }
+
+ public static StringRebuilder NewStringRebuilder(ITextChange change)
+ {
+ var textChange = change as TextChange;
+ return (textChange != null) ? textChange._newText : StringRebuilder.Create(change.NewText);
+ }
+
+ public static StringRebuilder ChangeOldSubText(ITextChange change, int start, int length)
+ {
+ var textChange = change as TextChange;
+ if (textChange != null)
+ return textChange._oldText.GetSubText(new Span(start, length));
+
+ var change3 = change as ITextChange3;
+ if (change3 != null)
+ return StringRebuilder.Create(change3.GetOldText(new Span(start, length)));
+
+ return StringRebuilder.Create(change.OldText.Substring(start, length));
+ }
+
+ public static StringRebuilder ChangeNewSubText(ITextChange change, int start, int length)
+ {
+ var textChange = change as TextChange;
+ if (textChange != null)
+ return textChange._newText.GetSubText(new Span(start, length));
+
+ var change3 = change as ITextChange3;
+ if (change3 != null)
+ return StringRebuilder.Create(change3.GetNewText(new Span(start, length)));
+
+ return StringRebuilder.Create(change.NewText.Substring(start, length));
+ }
+
+ public static string ChangeOldSubstring(ITextChange change, int start, int length)
+ {
+ var textChange = change as TextChange;
+ if (textChange != null)
+ return textChange._oldText.GetText(new Span(start, length));
+
+ var change3 = change as ITextChange3;
+ if (change3 != null)
+ return change3.GetOldText(new Span(start, length));
+
+ return change.OldText.Substring(start, length);
+ }
+
+ public static string ChangeNewSubstring(ITextChange change, int start, int length)
+ {
+ var textChange = change as TextChange;
+ if (textChange != null)
+ return textChange._newText.GetText(new Span(start, length));
+
+ var change3 = change as ITextChange3;
+ if (change3 != null)
+ return change3.GetNewText(new Span(start, length));
+
+ return change.NewText.Substring(start, length);
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextDocument.cs b/src/Text/Impl/TextModel/TextDocument.cs
new file mode 100644
index 0000000..e546e97
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextDocument.cs
@@ -0,0 +1,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 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("textBuffer");
+ }
+ if (filePath == null)
+ {
+ throw new ArgumentNullException("filePath");
+ }
+ if (textDocumentFactoryService == null)
+ {
+ throw new ArgumentNullException("textDocumentFactoryService");
+ }
+ if (encoding == null)
+ {
+ throw new ArgumentNullException("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("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, _filePath, 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("filePath");
+ }
+
+ PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder);
+ UpdateSaveStatus(filePath, _filePath != filePath);
+
+ // 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("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("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("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; }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextDocumentFactoryService.cs b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs
new file mode 100644
index 0000000..ff59ae3
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs
@@ -0,0 +1,340 @@
+//
+// 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.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ [Export(typeof(ITextDocumentFactoryService))]
+ internal sealed partial class TextDocumentFactoryService : ITextDocumentFactoryService
+ {
+ #region Internal Consumptions
+
+ [Import]
+ internal ITextBufferFactoryService BufferFactoryService { get; set; }
+
+ [ImportMany]
+ internal List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> UnorderedEncodingDetectors { get; set; }
+
+ [Import]
+ internal GuardedOperations GuardedOperations { get; set; }
+
+ #endregion
+
+ internal static Encoding DefaultEncoding = Encoding.Default; // Exposed for unit tests.
+
+ #region ITextDocumentFactoryService Members
+
+ public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType)
+ {
+ bool unused;
+ return CreateAndLoadTextDocument(filePath, contentType, attemptUtf8Detection: true, characterSubstitutionsOccurred: out unused);
+ }
+
+ public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType, Encoding encoding, out bool characterSubstitutionsOccurred)
+ {
+ if (filePath == null)
+ {
+ throw new ArgumentNullException("filePath");
+ }
+
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ if (encoding == null)
+ {
+ throw new ArgumentNullException("encoding");
+ }
+
+ var fallbackDetector = new FallbackDetector(encoding.DecoderFallback);
+ var modifiedEncoding = (Encoding)encoding.Clone();
+ modifiedEncoding.DecoderFallback = fallbackDetector;
+
+ ITextBuffer buffer;
+ DateTime lastModified;
+ long fileSize;
+ using (Stream stream = OpenFile(filePath, out lastModified, out fileSize))
+ {
+ // Caller knows best, so don't use byte order marks.
+ using (StreamReader reader = new StreamReader(stream, modifiedEncoding, detectEncodingFromByteOrderMarks: false))
+ {
+ System.Diagnostics.Debug.Assert(encoding.CodePage == reader.CurrentEncoding.CodePage);
+ buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath);
+ }
+ }
+
+ characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred;
+
+#if _DEBUG
+ TextUtilities.TagBuffer(buffer, filePath);
+#endif
+ TextDocument textDocument = new TextDocument(buffer, filePath, lastModified, this, encoding, explicitEncoding: true);
+
+ RaiseTextDocumentCreated(textDocument);
+
+ return textDocument;
+ }
+
+ public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType, bool attemptUtf8Detection, out bool characterSubstitutionsOccurred)
+ {
+ if (filePath == null)
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
+
+ if (contentType == null)
+ {
+ throw new ArgumentNullException(nameof(contentType));
+ }
+
+ characterSubstitutionsOccurred = false;
+
+ Encoding chosenEncoding = null;
+ ITextBuffer buffer = null;
+ DateTime lastModified;
+ long fileSize;
+
+ // select matching detectors without instantiating any
+ var detectors = ExtensionSelector.SelectMatchingExtensions(OrderedEncodingDetectors, contentType);
+
+ using (Stream stream = OpenFile(filePath, out lastModified, out fileSize))
+ {
+ // First, look for a byte order marker and let the encoding detecters
+ // suggest encodings.
+ chosenEncoding = EncodedStreamReader.DetectEncoding(stream, detectors, GuardedOperations);
+
+ // If that didn't produce a result, tentatively try to open as UTF 8.
+ if (chosenEncoding == null && attemptUtf8Detection)
+ {
+ try
+ {
+ var detectorEncoding = new ExtendedCharacterDetector();
+
+ using (StreamReader reader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, detectorEncoding, false))
+ {
+ buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath);
+ characterSubstitutionsOccurred = false;
+ }
+
+ if (detectorEncoding.DecodedExtendedCharacters)
+ {
+ // Valid UTF-8 but has bytes that are not merely ASCII.
+ chosenEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ }
+ else
+ {
+ // Valid UTF8 but no extended characters, so it's valid ASCII.
+ // We don't use ASCII here because of the following scenario:
+ // The user with a non-ENU system encoding opens a code file with ASCII-only contents
+ chosenEncoding = DefaultEncoding;
+ }
+ }
+ catch (DecoderFallbackException)
+ {
+ // Not valid UTF-8.
+ // Proceed to the next if block to try the system's default codepage.
+ Debug.Assert(buffer == null);
+ buffer = null;
+ stream.Position = 0;
+ }
+ }
+
+ Debug.Assert(buffer == null || chosenEncoding != null);
+
+ // If all else didn't work, use system's default encoding.
+ if (chosenEncoding == null)
+ {
+ chosenEncoding = DefaultEncoding;
+ }
+
+ if (buffer == null)
+ {
+ var fallbackDetector = new FallbackDetector(chosenEncoding.DecoderFallback);
+ var modifiedEncoding = (Encoding)chosenEncoding.Clone();
+ modifiedEncoding.DecoderFallback = fallbackDetector;
+
+ Debug.Assert(stream.Position == 0);
+
+ using (StreamReader reader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, modifiedEncoding, detectEncodingFromByteOrderMarks: false))
+ {
+ Debug.Assert(chosenEncoding.CodePage == reader.CurrentEncoding.CodePage);
+ buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath);
+ }
+
+ characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred;
+ }
+ }
+
+ TextDocument textDocument = new TextDocument(buffer, filePath, lastModified, this, chosenEncoding, attemptUtf8Detection: attemptUtf8Detection);
+
+ RaiseTextDocumentCreated(textDocument);
+
+ return textDocument;
+ }
+
+ public ITextDocument CreateTextDocument(ITextBuffer textBuffer, string filePath)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ if (filePath == null)
+ {
+ throw new ArgumentNullException("filePath");
+ }
+
+ TextDocument textDocument = new TextDocument(textBuffer, filePath, DateTime.UtcNow, this, Encoding.UTF8);
+ RaiseTextDocumentCreated(textDocument);
+
+ return textDocument;
+ }
+
+ public bool TryGetTextDocument(ITextBuffer textBuffer, out ITextDocument textDocument)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException("textBuffer");
+ }
+
+ textDocument = null;
+
+ TextDocument document;
+ if (textBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document))
+ {
+ if(document != null && !document.IsDisposed)
+ {
+ textDocument = document;
+ return true;
+ }
+ else
+ {
+ Debug.Fail("There shouldn't be a null or disposed document in the buffer's property bag. Did someone else put it there?");
+ }
+ }
+
+ return false;
+ }
+
+ public event EventHandler<TextDocumentEventArgs> TextDocumentCreated;
+
+ public event EventHandler<TextDocumentEventArgs> TextDocumentDisposed;
+
+ #endregion
+
+ #region helpers
+
+ /// <summary>
+ /// Helper method to raise the <see cref="ITextDocumentFactoryService.TextDocumentCreated"/> event.
+ /// </summary>
+ /// <param name="textDocument">The <see cref="ITextDocument"/> that was created.</param>
+ private void RaiseTextDocumentCreated(ITextDocument textDocument)
+ {
+ EventHandler<TextDocumentEventArgs> documentCreated = this.TextDocumentCreated;
+ if (documentCreated != null)
+ {
+ documentCreated.Invoke(this, new TextDocumentEventArgs(textDocument));
+ }
+ }
+
+ /// <summary>
+ /// Helper method to raise the <see cref="ITextDocumentFactoryService.TextDocumentDisposed"/> event.
+ /// </summary>
+ /// <param name="textDocument">The <see cref="ITextDocument"/> that was disposed.</param>
+ internal void RaiseTextDocumentDisposed(ITextDocument textDocument)
+ {
+ EventHandler<TextDocumentEventArgs> documentDisposed = this.TextDocumentDisposed;
+ if (documentDisposed != null)
+ {
+ documentDisposed.Invoke(this, new TextDocumentEventArgs(textDocument));
+ }
+ }
+
+ private IList<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> _orderedEncodingDetectors;
+
+ internal IEnumerable<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> OrderedEncodingDetectors
+ {
+ get
+ {
+ if (_orderedEncodingDetectors == null)
+ {
+ if (UnorderedEncodingDetectors != null)
+ {
+ _orderedEncodingDetectors = Orderer.Order(UnorderedEncodingDetectors);
+ }
+ else
+ {
+ _orderedEncodingDetectors = new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>();
+ }
+ }
+ return _orderedEncodingDetectors;
+ }
+ set // for unit test helper.
+ {
+ _orderedEncodingDetectors = new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>(value);
+ }
+ }
+
+ // Exposed for testing.
+ internal Func<string, Stream> StreamCreator;
+
+ private Stream OpenFile(string filePath, out DateTime lastModifiedTimeUtc, out long fileSize)
+ {
+ if (StreamCreator != null)
+ {
+ lastModifiedTimeUtc = DateTime.UtcNow;
+ fileSize = -1; // a signal that the file size is not known
+ return StreamCreator(filePath);
+ }
+ else
+ {
+ return OpenFileGuts(filePath, out lastModifiedTimeUtc, out fileSize);
+ }
+ }
+
+ internal static Stream OpenFileGuts(string filePath, out DateTime lastModifiedTimeUtc, out long fileSize)
+ {
+ // Sometimes files are held open with FILE_FLAG_DELETE_ON_CLOSE before the editor
+ // is asked to open them. We should support that by allowing FileShare.Delete.
+ Stream result = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
+ FileInfo fileInfo = new FileInfo(filePath);
+ lastModifiedTimeUtc = fileInfo.LastWriteTimeUtc;
+ fileSize = fileInfo.Length;
+ if (fileSize > int.MaxValue)
+ {
+ throw new InvalidOperationException(Strings.FileTooLarge);
+ }
+
+ return result;
+ }
+
+ // For unit testing purposes
+ internal void Initialize(ITextBufferFactoryService bufferFactoryService)
+ {
+ Initialize(bufferFactoryService, null);
+ }
+
+ internal void Initialize(ITextBufferFactoryService bufferFactoryService, List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> detectors)
+ {
+ BufferFactoryService = bufferFactoryService;
+ UnorderedEncodingDetectors = detectors ?? new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextImageVersion.cs b/src/Text/Impl/TextModel/TextImageVersion.cs
new file mode 100644
index 0000000..ef3e44a
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextImageVersion.cs
@@ -0,0 +1,127 @@
+//
+// 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.
+//
+using System;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Text.Implementation
+{
+ /// <summary>
+ /// An immutable variation on the StringBuilder class.
+ /// </summary>
+ internal class TextImageVersion : ITextImageVersion
+ {
+ public TextImageVersion(int length)
+ : this(versionNumber: 0, reiteratedVersionNumber: 0, length: length, identifier: new object())
+ {
+ }
+
+ private TextImageVersion(int versionNumber, int reiteratedVersionNumber, int length, object identifier)
+ {
+ this.VersionNumber = versionNumber;
+ this.ReiteratedVersionNumber = reiteratedVersionNumber;
+ this.Identifier = identifier;
+ this.Length = length;
+ }
+
+ internal TextImageVersion CreateNext(int reiteratedVersionNumber, int length, INormalizedTextChangeCollection changes)
+ {
+ int newVersionNumber = this.VersionNumber + 1;
+
+ if (reiteratedVersionNumber < 0)
+ {
+ // If there are no changes (e.g. readonly region edit or content type change), then
+ // we consider this a reiteration of the current version. changes can be null in the special case
+ // of doing a reload (at which point the reload code will call SetChanges after computing the diff).
+ reiteratedVersionNumber = ((changes != null) && (changes.Count == 0)) ? this.ReiteratedVersionNumber : newVersionNumber;
+ }
+ else if (reiteratedVersionNumber > newVersionNumber)
+ {
+ throw new ArgumentOutOfRangeException(nameof(reiteratedVersionNumber));
+ }
+
+ if (length == -1)
+ {
+ length = this.Length;
+ int changeCount = changes.Count;
+ for (int c = 0; c < changeCount; ++c)
+ {
+ length += changes[c].Delta;
+ }
+ }
+
+ var newVersion = new TextImageVersion(newVersionNumber, reiteratedVersionNumber, length, this.Identifier);
+
+ // Arguably this should happen as an atomic operation but it is unlikely to cause a race condition
+ // because, in general, people won't even be looking at these properties until they get a change event
+ // (which happens after everything has been set).
+ this.SetChanges(changes);
+ this.Next = newVersion;
+
+ return newVersion;
+ }
+
+ // The length needs to be set after the creation of the version in some cases (projection, for example).
+ internal void SetLength(int length)
+ {
+ if (this.Length != 0)
+ throw new InvalidOperationException("Not allowed to SetLength twice");
+
+ this.Length = length;
+ }
+
+ internal void SetChanges(INormalizedTextChangeCollection changes)
+ {
+ if (this.Changes != null)
+ throw new InvalidOperationException("Not allowed to SetChanges twice");
+
+ this.Changes = changes;
+ }
+
+ #region ITextImageVersion members
+ public ITextImageVersion Next { get; private set; }
+
+ public int Length { get; private set; }
+
+ public INormalizedTextChangeCollection Changes { get; private set; }
+
+ public int VersionNumber { get; }
+
+ public int ReiteratedVersionNumber { get; }
+
+ public object Identifier { get; }
+
+ public int TrackTo(VersionedPosition other, PointTrackingMode mode)
+ {
+ if (other.Version == null)
+ throw new ArgumentException(nameof(other));
+
+ if (other.Version.VersionNumber == this.VersionNumber)
+ return other.Position;
+
+ if (other.Version.VersionNumber > this.VersionNumber)
+ return Tracking.TrackPositionForwardInTime(mode, other.Position, this, other.Version);
+ else
+ return Tracking.TrackPositionBackwardInTime(mode, other.Position, this, other.Version);
+ }
+
+ public Span TrackTo(VersionedSpan span, SpanTrackingMode mode)
+ {
+ if (span.Version == null)
+ throw new ArgumentException(nameof(span));
+
+ if (span.Version.VersionNumber == this.VersionNumber)
+ return span.Span;
+
+ if (span.Version.VersionNumber > this.VersionNumber)
+ return Tracking.TrackSpanForwardInTime(mode, span.Span, this, span.Version);
+ else
+ return Tracking.TrackSpanBackwardInTime(mode, span.Span, this, span.Version);
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextModelUtilities.cs b/src/Text/Impl/TextModel/TextModelUtilities.cs
new file mode 100644
index 0000000..8e42fb2
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextModelUtilities.cs
@@ -0,0 +1,61 @@
+//
+// 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 Microsoft.VisualStudio.Text.Utilities;
+
+ internal static class TextModelUtilities
+ {
+ static public int ComputeLineCountDelta(LineBreakBoundaryConditions boundaryConditions, StringRebuilder oldText, StringRebuilder newText)
+ {
+ int delta = 0;
+ delta -= oldText.LineBreakCount;
+ delta += newText.LineBreakCount;
+ if ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0)
+ {
+ if (oldText.FirstCharacter == '\n')
+ {
+ delta++;
+ }
+ if (newText.FirstCharacter == '\n')
+ {
+ delta--;
+ }
+ }
+
+ if ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0)
+ {
+ if (oldText.LastCharacter == '\r')
+ {
+ delta++;
+ }
+ if (newText.LastCharacter == '\r')
+ {
+ delta--;
+ }
+ }
+
+ if ((oldText.Length == 0) &&
+ ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0) &&
+ ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0))
+ {
+ // return and newline were adjacent before and were separated by the insertion
+ delta++;
+ }
+
+ if ((newText.Length == 0) &&
+ ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0) &&
+ ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0))
+ {
+ // return and newline were separated before and were made adjacent by the deletion
+ delta--;
+ }
+ return delta;
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextSnapshot.cs b/src/Text/Impl/TextModel/TextSnapshot.cs
new file mode 100644
index 0000000..c331e01
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextSnapshot.cs
@@ -0,0 +1,33 @@
+//
+// 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.Collections.Generic;
+
+ internal partial class TextSnapshot : BaseSnapshot, ITextSnapshot, ITextSnapshot2
+ {
+ #region Private members
+ private readonly ITextBuffer textBuffer;
+ #endregion
+
+ #region Constructors
+ public TextSnapshot(ITextBuffer textBuffer, ITextVersion2 version, StringRebuilder content)
+ : base(version, content)
+ {
+ System.Diagnostics.Debug.Assert(version.Length == content.Length);
+ this.textBuffer = textBuffer;
+ }
+ #endregion
+
+ protected override ITextBuffer TextBufferHelper
+ {
+ get { return this.textBuffer; }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextSnapshotLine.cs b/src/Text/Impl/TextModel/TextSnapshotLine.cs
new file mode 100644
index 0000000..c2a76e6
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextSnapshotLine.cs
@@ -0,0 +1,164 @@
+//
+// 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;
+
+ internal partial class TextSnapshotLine : ITextSnapshotLine
+ {
+ private readonly int lineNumber;
+ private readonly int lineBreakLength;
+ private readonly SnapshotSpan extent;
+
+ public TextSnapshotLine(ITextSnapshot snapshot, int lineNumber, Span extent, int lineBreakLength)
+ {
+ this.extent = new SnapshotSpan(snapshot, extent);
+
+ //This is inner loop code called only from private methods so we don't need to guard against bad data in released bits.
+ Debug.Assert(extent.End + lineBreakLength <= snapshot.Length);
+
+ this.lineNumber = lineNumber;
+ this.lineBreakLength = lineBreakLength;
+ }
+
+
+ public TextSnapshotLine(ITextSnapshot snapshot, TextImageLine lineSpan)
+ : this(snapshot, lineSpan.LineNumber, lineSpan.Extent, lineSpan.LineBreakLength)
+ {
+ }
+
+ public TextSnapshotLine(ITextSnapshot snapshot, Tuple<int, Span, int> lineSpan)
+ : this(snapshot, lineSpan.Item1, lineSpan.Item2, lineSpan.Item3)
+ {
+ }
+
+ /// <summary>
+ /// ITextSnapshot in which the line appears.
+ /// </summary>
+ public ITextSnapshot Snapshot
+ {
+ get { return this.extent.Snapshot; }
+ }
+
+ /// <summary>
+ /// The 0-origin line number of the line.
+ /// </summary>
+ public int LineNumber
+ {
+ get
+ {
+ return this.lineNumber;
+ }
+ }
+
+ /// <summary>
+ /// Position in TextBuffer of the first character in the line.
+ /// </summary>
+ public SnapshotPoint Start
+ {
+ get { return this.extent.Start; }
+ }
+
+ /// <summary>
+ /// Length of the line, excluding any line break characters.
+ /// </summary>
+ public int Length
+ {
+ get { return this.extent.Length; }
+ }
+
+ /// <summary>
+ /// Length of the line, including any line break characters.
+ /// </summary>
+ public int LengthIncludingLineBreak
+ {
+ get { return this.extent.Length + this.lineBreakLength; }
+ }
+
+ /// <summary>
+ /// Length of line break characters (always falls in the range [0..2])
+ /// </summary>
+ public int LineBreakLength
+ {
+ get { return this.lineBreakLength; }
+ }
+
+ /// <summary>
+ /// The position of the first character past the end of the line, excluding any
+ /// line break characters (thus will address a line break character, except
+ /// for the last line in the buffer).
+ /// </summary>
+ public SnapshotPoint End
+ {
+ get { return this.extent.End; }
+ }
+
+ /// <summary>
+ /// The position of the first character past the end of the line, including any
+ /// line break characters (thus will address the first character in
+ /// the succeeding line, unless this is the last line).
+ /// </summary>
+ public SnapshotPoint EndIncludingLineBreak
+ {
+ get { return new SnapshotPoint(this.extent.Snapshot, this.extent.Span.End + this.lineBreakLength); }
+ }
+
+ /// <summary>
+ /// The extent of the line, excluding any line break characters.
+ /// </summary>
+ public SnapshotSpan Extent
+ {
+ get
+ {
+ return this.extent;
+ }
+ }
+
+ /// <summary>
+ /// The extent of the line, including any line break characters.
+ /// </summary>
+ public SnapshotSpan ExtentIncludingLineBreak
+ {
+ get { return new SnapshotSpan(this.extent.Start, this.LengthIncludingLineBreak); }
+ }
+
+ /// <summary>
+ /// The text of the line, excluding any line break characters.
+ /// May return incorrect results or fail if the text buffer has changed since the
+ /// ITextBufferLine was created.
+ /// </summary>
+ public string GetText()
+ {
+ return this.Extent.GetText();
+ }
+
+ /// <summary>
+ /// The text of the line, including any line break characters.
+ /// May return incorrect results of fail if the text buffer has changed since the
+ /// ITextBufferLine was created.
+ /// </summary>
+ /// <returns></returns>
+ public string GetTextIncludingLineBreak()
+ {
+ return this.ExtentIncludingLineBreak.GetText();
+ }
+
+ /// <summary>
+ /// The string consisting of the line break characters (if any) at the
+ /// end of the line. Has zero length for the last line in the buffer.
+ /// May return incorrect results of fail if the text buffer has changed since the
+ /// ITextBufferLine was created.
+ /// </summary>
+ /// <returns></returns>
+ public string GetLineBreakText()
+ {
+ return this.extent.Snapshot.GetText(new Span(this.Extent.Span.End, this.lineBreakLength));
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TextVersion.cs b/src/Text/Impl/TextModel/TextVersion.cs
new file mode 100644
index 0000000..55727c5
--- /dev/null
+++ b/src/Text/Impl/TextModel/TextVersion.cs
@@ -0,0 +1,189 @@
+//
+// 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;
+
+ /// <summary>
+ /// An internal implementation of ITextVersion
+ /// </summary>
+ internal partial class TextVersion : ITextVersion, ITextVersion2
+ {
+ private readonly TextImageVersion _textImageVersion;
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="TextVersion"/>.
+ /// </summary>
+ /// <param name="textBuffer">The <see cref="ITextBuffer"/> to which the version belongs.</param>
+ /// <param name="imageVersion">The <see cref="ITextImageVersion"/> of the associated snapshot.</param>
+ public TextVersion(ITextBuffer textBuffer, TextImageVersion imageVersion)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException(nameof(textBuffer));
+ }
+
+ if (imageVersion == null)
+ {
+ throw new ArgumentNullException(nameof(imageVersion));
+ }
+
+ this.TextBuffer = textBuffer;
+ _textImageVersion = imageVersion;
+ }
+
+ /// <summary>
+ /// Create a new version based on applying <paramref name="changes"/> to this.
+ /// </summary>
+ /// <param name="changes">null if set later</param>
+ /// <param name="newLength">use -1 to compute a length</param>
+ /// <param name="reiteratedVersionNumber">use -1 to get the default value</param>
+ /// <remarks>
+ /// <para>If <paramref name="changes"/> can be null, then <paramref name="newLength"/> cannot be -1.</para>
+ /// </remarks>
+ internal TextVersion CreateNext(INormalizedTextChangeCollection changes, int newLength = -1, int reiteratedVersionNumber = -1)
+ {
+ if (this.Next != null)
+ throw new InvalidOperationException("Not allowed to CreateNext twice");
+
+ var newTextImageVersion = this._textImageVersion.CreateNext(reiteratedVersionNumber: reiteratedVersionNumber, length: newLength, changes: changes);
+
+ var next = new TextVersion(this.TextBuffer, newTextImageVersion);
+ this.Next = next;
+
+ return next;
+ }
+
+ internal void SetLength(int length)
+ {
+ _textImageVersion.SetLength(length);
+ }
+
+ internal void SetChanges(INormalizedTextChangeCollection changes)
+ {
+ _textImageVersion.SetChanges(changes);
+ }
+
+ public ITextBuffer TextBuffer
+ {
+ get;
+ }
+
+ public int VersionNumber
+ {
+ get { return this.ImageVersion.VersionNumber; }
+ }
+
+ public int ReiteratedVersionNumber
+ {
+ get { return this.ImageVersion.ReiteratedVersionNumber; }
+ }
+
+ /// <summary>
+ /// Gets the next version node
+ /// </summary>
+ public ITextVersion Next
+ {
+ get; private set;
+ }
+
+ /// <summary>
+ /// Gets the current change information
+ /// </summary>
+ public INormalizedTextChangeCollection Changes
+ {
+ get { return this.ImageVersion.Changes; }
+ }
+
+ public int Length
+ {
+ get { return this.ImageVersion.Length; }
+ }
+
+ public ITextImageVersion ImageVersion { get { return _textImageVersion; } }
+
+ #region Point and Span Factories
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
+ {
+ // Forward fidelity is implicit
+ return new ForwardFidelityTrackingPoint(this, position, trackingMode);
+ }
+
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ if (trackingFidelity == TrackingFidelityMode.Forward)
+ {
+ return new ForwardFidelityTrackingPoint(this, position, trackingMode);
+ }
+ else
+ {
+ return new HighFidelityTrackingPoint(this, position, trackingMode, trackingFidelity);
+ }
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode)
+ {
+ // Forward fidelity is implicit
+ if (trackingMode == SpanTrackingMode.Custom)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ return new ForwardFidelityTrackingSpan(this, new Span(start, length), trackingMode);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ return CreateTrackingSpan(new Span(start, length), trackingMode, trackingFidelity);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode)
+ {
+ // Forward fidelity is implicit
+ if (trackingMode == SpanTrackingMode.Custom)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ return new ForwardFidelityTrackingSpan(this, span, trackingMode);
+ }
+
+ public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ if (trackingMode == SpanTrackingMode.Custom)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+ if (trackingFidelity == TrackingFidelityMode.Forward)
+ {
+ return new ForwardFidelityTrackingSpan(this, span, trackingMode);
+ }
+ else
+ {
+ return new HighFidelityTrackingSpan(this, span, trackingMode, trackingFidelity);
+ }
+ }
+
+ public ITrackingSpan CreateCustomTrackingSpan(Span span, TrackingFidelityMode trackingFidelity, object customState, CustomTrackToVersion behavior)
+ {
+ if (behavior == null)
+ {
+ throw new ArgumentNullException("behavior");
+ }
+ if (trackingFidelity != TrackingFidelityMode.Forward)
+ {
+ throw new NotImplementedException();
+ }
+ return new ForwardFidelityCustomTrackingSpan(this, span, customState, behavior);
+ }
+ #endregion
+
+ public override string ToString()
+ {
+ return String.Format("V{0} (r{1})", VersionNumber, ReiteratedVersionNumber);
+ }
+ }
+}
diff --git a/src/Text/Impl/TextModel/TrackingPoint.cs b/src/Text/Impl/TextModel/TrackingPoint.cs
new file mode 100644
index 0000000..02750f3
--- /dev/null
+++ b/src/Text/Impl/TextModel/TrackingPoint.cs
@@ -0,0 +1,101 @@
+//
+// 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;
+
+ /// <summary>
+ /// Base class for a tracking position in a particular <see cref="ITextBuffer"/>.
+ /// </summary>
+ internal abstract partial class TrackingPoint : ITrackingPoint
+ {
+ #region State and Construction
+ protected readonly PointTrackingMode trackingMode;
+
+ protected TrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException("version");
+ }
+ if (position < 0 | position > version.Length)
+ {
+ throw new ArgumentOutOfRangeException("position");
+ }
+ if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+
+ this.trackingMode = trackingMode;
+ }
+ #endregion
+
+ #region ITrackingPoint members
+ public abstract ITextBuffer TextBuffer { get; }
+
+ public PointTrackingMode TrackingMode
+ {
+ get { return this.trackingMode; }
+ }
+
+ public abstract TrackingFidelityMode TrackingFidelity { get; }
+
+ public int GetPosition(ITextVersion version)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException("version");
+ }
+ if (version.TextBuffer != this.TextBuffer)
+ {
+ throw new ArgumentException(Strings.InvalidVersion);
+ }
+ return TrackPosition(version);
+ }
+
+ public int GetPosition(ITextSnapshot snapshot)
+ {
+ if (snapshot == null)
+ {
+ throw new ArgumentNullException("snapshot");
+ }
+ if (snapshot.TextBuffer != this.TextBuffer)
+ {
+ throw new ArgumentException(Strings.InvalidSnapshot);
+ }
+ return TrackPosition(snapshot.Version);
+ }
+
+ public SnapshotPoint GetPoint(ITextSnapshot snapshot)
+ {
+ return new SnapshotPoint(snapshot, GetPosition(snapshot));
+ }
+
+ public char GetCharacter(ITextSnapshot snapshot)
+ {
+ return GetPoint(snapshot).GetChar();
+ }
+ #endregion
+
+ protected abstract int TrackPosition(ITextVersion targetVersion);
+
+ #region Diagnostic Support
+ protected static string PointTrackingModeToString(PointTrackingMode trackingMode)
+ {
+ return trackingMode == PointTrackingMode.Positive ? "→" : "←";
+ }
+
+ protected static string ToString(ITextVersion version, int position, PointTrackingMode trackingMode)
+ {
+ return string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0} {2}@{1}",
+ version.VersionNumber, position, PointTrackingModeToString(trackingMode));
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/TrackingSpan.cs b/src/Text/Impl/TextModel/TrackingSpan.cs
new file mode 100644
index 0000000..8ffb5fe
--- /dev/null
+++ b/src/Text/Impl/TextModel/TrackingSpan.cs
@@ -0,0 +1,125 @@
+//
+// 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;
+
+ /// <summary>
+ /// Base class for a tracking span in a particular <see cref="ITextBuffer"/>.
+ /// </summary>
+ internal abstract partial class TrackingSpan : ITrackingSpan
+ {
+ #region State and Construction
+ protected readonly SpanTrackingMode trackingMode;
+
+ public TrackingSpan(ITextVersion version, Span span, SpanTrackingMode trackingMode)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException("version");
+ }
+ if (span.End > version.Length)
+ {
+ throw new ArgumentOutOfRangeException("span");
+ }
+ if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom)
+ {
+ throw new ArgumentOutOfRangeException("trackingMode");
+ }
+
+ this.trackingMode = trackingMode;
+ }
+ #endregion
+
+ #region ITrackingSpan members
+ public abstract ITextBuffer TextBuffer { get; }
+
+ public SpanTrackingMode TrackingMode
+ {
+ get { return this.trackingMode; }
+ }
+
+ public abstract TrackingFidelityMode TrackingFidelity { get; }
+
+ public Span GetSpan(ITextVersion version)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException("version");
+ }
+ if (version.TextBuffer != this.TextBuffer)
+ {
+ throw new ArgumentException(Strings.InvalidVersion);
+ }
+ return TrackSpan(version);
+ }
+
+ public SnapshotSpan GetSpan(ITextSnapshot snapshot)
+ {
+ if (snapshot == null)
+ {
+ throw new ArgumentNullException("snapshot");
+ }
+ if (snapshot.TextBuffer != this.TextBuffer)
+ {
+ throw new ArgumentException(Strings.InvalidSnapshot);
+ }
+
+ return new SnapshotSpan(snapshot, TrackSpan(snapshot.Version));
+ }
+
+ public SnapshotPoint GetStartPoint(ITextSnapshot snapshot)
+ {
+ SnapshotSpan s = this.GetSpan(snapshot);
+ return new SnapshotPoint(snapshot, s.Start);
+ }
+
+ public SnapshotPoint GetEndPoint(ITextSnapshot snapshot)
+ {
+ SnapshotSpan s = this.GetSpan(snapshot);
+ return new SnapshotPoint(snapshot, s.End);
+ }
+
+ public string GetText(ITextSnapshot snapshot)
+ {
+ return GetSpan(snapshot).GetText();
+ }
+ #endregion
+
+ #region Helpers
+ protected abstract Span TrackSpan(ITextVersion targetVersion);
+ #endregion
+
+ #region Diagnostic Support
+ protected static string SpanTrackingModeToString(SpanTrackingMode trackingMode)
+ {
+ switch (trackingMode)
+ {
+ case SpanTrackingMode.EdgeExclusive:
+ return "→←";
+ case SpanTrackingMode.EdgeInclusive:
+ return "←→";
+ case SpanTrackingMode.EdgeNegative:
+ return "←←";
+ case SpanTrackingMode.EdgePositive:
+ return "→→";
+ case SpanTrackingMode.Custom:
+ return "custom";
+ default:
+ return "??";
+ }
+ }
+
+ protected static string ToString(ITextVersion version, Span span, SpanTrackingMode trackingMode)
+ {
+ return string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0} {2}@{1}",
+ version.VersionNumber, span.ToString(), SpanTrackingModeToString(trackingMode));
+ }
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs
new file mode 100644
index 0000000..bb70ebd
--- /dev/null
+++ b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs
@@ -0,0 +1,236 @@
+//
+// 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;
+
+ /// <summary>
+ /// A compact representation of a normalized text change collection containing a single change that is the
+ /// insertion, deletion, or replacement of one character, with no effect on line counts. This object embodies both the
+ /// collection and its single member.
+ /// </summary>
+ internal partial class TrivialNormalizedTextChangeCollection : INormalizedTextChangeCollection, ITextChange3
+ {
+ char data;
+ bool isInsertion;
+ int position;
+
+ public TrivialNormalizedTextChangeCollection(char data, bool isInsertion, int position)
+ {
+ this.data = data;
+ this.isInsertion = isInsertion;
+ this.position = position;
+ }
+
+ public bool IncludesLineChanges
+ {
+ get { return false; }
+ }
+
+ #region IList<ITextChange> implementation
+ public ITextChange this[int index]
+ {
+ get
+ {
+ if (index != 0)
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+ return this;
+ }
+ set
+ {
+ throw new System.NotSupportedException();
+ }
+ }
+
+ public void Insert(int index, ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ throw new System.NotSupportedException();
+ }
+ #endregion
+
+ #region ICollection<ITextChange> implementation
+ public int Count
+ {
+ get { return 1; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public void Add(ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+
+ public void Clear()
+ {
+ throw new System.NotSupportedException();
+ }
+
+ public int IndexOf(ITextChange item)
+ {
+ return item == this ? 0 : -1;
+ }
+
+ public bool Contains(ITextChange item)
+ {
+ return item == this;
+ }
+
+ public void CopyTo(ITextChange[] array, int arrayIndex)
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException("array");
+ }
+ if (arrayIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException("arrayIndex");
+ }
+ if (array.Rank > 1 || arrayIndex >= array.Length)
+ {
+ throw new ArgumentException("Bad arguments to CopyTo");
+ }
+ array[arrayIndex] = this;
+ }
+
+ public bool Remove(ITextChange item)
+ {
+ throw new System.NotSupportedException();
+ }
+ #endregion
+
+ #region IEnumerable<ITextChange> implementation
+ public System.Collections.Generic.IEnumerator<ITextChange> GetEnumerator()
+ {
+ yield return this;
+ }
+ #endregion
+
+ #region IEnumerable implementation
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ yield return this;
+ }
+ #endregion
+
+ #region ITextChange implementation
+ Span ITextChange.OldSpan
+ {
+ get { return new Span(position, isInsertion ? 0 : 1); }
+ }
+
+ Span ITextChange.NewSpan
+ {
+ get { return new Span(position, isInsertion ? 1 : 0); }
+ }
+
+ int ITextChange.OldPosition
+ {
+ get { return position; }
+ }
+
+ int ITextChange.NewPosition
+ {
+ get { return position; }
+ }
+
+ int ITextChange.Delta
+ {
+ get { return isInsertion ? +1 : -1; }
+ }
+
+ int ITextChange.OldEnd
+ {
+ get { return isInsertion ? position : position + 1; }
+ }
+
+ int ITextChange.NewEnd
+ {
+ get { return isInsertion ? position + 1 : position; }
+ }
+
+ string ITextChange.OldText
+ {
+ get { return isInsertion ? "" : new string(data, 1); }
+ }
+
+ string ITextChange.NewText
+ {
+ get { return isInsertion ? new string(data, 1) : ""; }
+ }
+
+ int ITextChange.OldLength
+ {
+ get { return isInsertion ? 0 : 1; }
+ }
+
+ int ITextChange.NewLength
+ {
+ get { return isInsertion ? 1 : 0; }
+ }
+
+ int ITextChange.LineCountDelta
+ {
+ get { return 0; }
+ }
+
+ public bool IsOpaque { get; internal set; }
+
+ public string GetOldText(Span span)
+ {
+ if (span.End > 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(span));
+ }
+
+ return isInsertion ? "" : new string(data, span.Length);
+ }
+
+ public string GetNewText(Span span)
+ {
+ if (span.End > 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(span));
+ }
+
+ return isInsertion ? new string(data, span.Length) : "";
+ }
+
+ public char GetOldTextAt(int position)
+ {
+ if (position > 0 || isInsertion)
+ {
+ throw new ArgumentOutOfRangeException(nameof(position));
+ }
+
+ return data;
+ }
+
+ public char GetNewTextAt(int position)
+ {
+ if (position > 0 || !isInsertion)
+ {
+ throw new ArgumentOutOfRangeException(nameof(position));
+ }
+
+ return data;
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/Text/Impl/TextModel/VersionNumberPosition.cs b/src/Text/Impl/TextModel/VersionNumberPosition.cs
new file mode 100644
index 0000000..47ee7b2
--- /dev/null
+++ b/src/Text/Impl/TextModel/VersionNumberPosition.cs
@@ -0,0 +1,34 @@
+//
+// 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
+{
+ /// <summary>
+ /// Describe a version number and a position in that version. Used in the implementation
+ /// of high fidelity tracking points and spans.
+ /// </summary>
+ internal struct VersionNumberPosition
+ {
+ public int VersionNumber;
+ public int Position;
+ public VersionNumberPosition(int versionNumber, int position)
+ {
+ this.VersionNumber = versionNumber;
+ this.Position = position;
+ }
+ }
+
+ internal class VersionNumberPositionComparer : System.Collections.Generic.IComparer<VersionNumberPosition>
+ {
+ public int Compare(VersionNumberPosition x, VersionNumberPosition y)
+ {
+ return x.VersionNumber - y.VersionNumber; // both values are nonnegative, no overflow possible
+ }
+
+ static public VersionNumberPositionComparer Instance = new VersionNumberPositionComparer();
+ }
+}
diff --git a/src/Text/Impl/TextSearch/BackgroundSearch.cs b/src/Text/Impl/TextSearch/BackgroundSearch.cs
new file mode 100644
index 0000000..f0e6db9
--- /dev/null
+++ b/src/Text/Impl/TextSearch/BackgroundSearch.cs
@@ -0,0 +1,450 @@
+//
+// 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.Find.Implementation
+{
+ using Microsoft.VisualStudio.Text.Operations;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ /// <summary>
+ /// Performs text searches on lowest priority background threads and caches the results.
+ /// </summary>
+ /// <remarks>
+ /// The goal here is to be completely thread-safe: searches can be requested from any thread (and actually happen on background threads).
+ ///
+ /// Another goal is to never search more than we need to:
+ /// Once we've searched a section of the buffer we don't search it again unless it is modified.
+ /// Even if we get multiple, nearly simultaneous requests to search a section of the buffer, we only search it once.
+ /// </remarks>
+ internal class BackgroundSearch<T> : IDisposable where T : ITag
+ {
+ private ITextBuffer _buffer;
+ private readonly ITextSearchService2 _textSearchService;
+ private readonly string _searchTerm;
+ private readonly FindOptions _options;
+ public readonly Func<SnapshotSpan, T> TagFactory;
+ private readonly Action<ITextSnapshot, NormalizedSpanCollection> _callback;
+ private bool _isDisposed;
+
+ //This is used for locks so it can never be deleted/recreated. Internal for unit tests.
+ internal readonly Queue<NormalizedSnapshotSpanCollection> _requestQueue = new Queue<NormalizedSnapshotSpanCollection>();
+
+ //This needs to update atomically and is internal so unit tests can (more) easily test edge cases.
+ internal SearchResults _results;
+
+ public BackgroundSearch(ITextSearchService2 textSearchService, ITextBuffer buffer, string searchTerm, FindOptions options,
+ Func<SnapshotSpan, T> tagFactory, Action<ITextSnapshot, NormalizedSpanCollection> callback)
+ {
+ _textSearchService = textSearchService;
+ _buffer = buffer;
+
+ _searchTerm = searchTerm;
+ _options = options & ~FindOptions.SearchReverse; //The tagger ignores the reversed flag.
+ this.TagFactory = tagFactory;
+ _callback = callback;
+
+ _results = new SearchResults(_buffer.CurrentSnapshot, NormalizedSpanCollection.Empty, NormalizedSpanCollection.Empty);
+ }
+
+ public NormalizedSnapshotSpanCollection Results
+ {
+ get
+ {
+ var results = _results; //Snapshot results to avoid taking a lock.
+ return new NormalizedSnapshotSpanCollection(results.Snapshot, results.Matches);
+ }
+ }
+
+ /// <summary>
+ /// Kick off a background search if we don't have current results. Do nothing otherwise.
+ /// </summary>
+ /// <remarks>
+ /// This method can be called from any thread (though it will generally only be called from the UI thread).
+ /// </remarks>
+ public void QueueSearch(NormalizedSnapshotSpanCollection requestedSnapshotSpans)
+ {
+ Debug.Assert(requestedSnapshotSpans.Count > 0);
+
+ //Check to see if we have completely searched the current version of the text buffer
+ //and quickly abort since there is no point in queuing up another search if we have.
+ var results = _results; //Snapshot results to avoid taking a lock.
+ if (results.Snapshot == _buffer.CurrentSnapshot)
+ {
+ if ((results.SearchedSpans.Count == 1) && (results.SearchedSpans[0].Start == 0) && (results.SearchedSpans[0].Length == results.Snapshot.Length))
+ {
+ //We've searched the entire snapshot.
+ return;
+ }
+
+ if (requestedSnapshotSpans[0].Snapshot == results.Snapshot)
+ {
+ NormalizedSpanCollection unsearchedRequest = NormalizedSpanCollection.Difference(requestedSnapshotSpans, results.SearchedSpans);
+ if (unsearchedRequest.Count == 0)
+ {
+ return;
+ }
+ }
+ }
+
+ lock (_requestQueue)
+ {
+ _requestQueue.Enqueue(requestedSnapshotSpans);
+ if (_requestQueue.Count != 1)
+ {
+ //Request has been queued & we already have an active thread processing requests.
+ return;
+ }
+ }
+
+ Task.Factory.StartNew(this.ProcessQueue, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
+ }
+
+ #region Private Helpers
+ internal void ProcessQueue()
+ {
+ // Ensure the thread that is doing the work is both low priority and also background
+ try
+ {
+ Thread.CurrentThread.Priority = ThreadPriority.Lowest;
+
+ //Only one instance of this thread is running at a time, so we don't need to put locks around the bits that update
+ //our state (only the bits that play with the results queue).
+ while (true)
+ {
+ if (_isDisposed)
+ return;
+
+ NormalizedSnapshotSpanCollection request;
+ lock (_requestQueue)
+ {
+ //Do not dequeue the result here ... if a new request comes in while we are processing this request,
+ //we do not want to start a new thread.
+ request = _requestQueue.Peek();
+ }
+
+ //Always do searches on the current snapshot of the buffer, migrating results to that snapshot
+ //if needed.
+ ITextSnapshot snapshot = this.AdvanceToCurrentSnapshot();
+
+ NormalizedSpanCollection requestedSpans;
+ if (_options.HasFlag(FindOptions.Multiline))
+ {
+ //Multi-line searches are all or nothing.
+ if (_results.SearchedSpans.Count == 0)
+ {
+ requestedSpans = new NormalizedSpanCollection(new Span(0, snapshot.Length));
+ }
+ else
+ {
+ Debug.Assert((_results.SearchedSpans.Count == 1) && (_results.SearchedSpans[0].Start == 0) && (_results.SearchedSpans[0].End == snapshot.Length));
+ requestedSpans = NormalizedSpanCollection.Empty;
+ }
+ }
+ else
+ {
+ requestedSpans = BackgroundSearch<T>.TranslateToAndExtend(request[0].Snapshot, request, snapshot);
+
+ if (_results.SearchedSpans.Count > 0)
+ {
+ if ((_results.SearchedSpans.Count == 1) && (_results.SearchedSpans[0].Start == 0) && (_results.SearchedSpans[0].End == snapshot.Length))
+ {
+ //We've already got results for the entire buffer.
+ requestedSpans = NormalizedSpanCollection.Empty;
+ }
+ else
+ {
+ requestedSpans = NormalizedSpanCollection.Difference(requestedSpans, _results.SearchedSpans);
+ }
+ }
+ }
+
+ bool dequeueRequest = true;
+ if (requestedSpans.Count > 0)
+ {
+ IList<Span> newMatches = this.FindAll(snapshot, requestedSpans);
+
+ if (_isDisposed)
+ return;
+
+ if (snapshot == _buffer.CurrentSnapshot)
+ {
+ //The search completed without the buffer changing out from under us, add in the new results.
+ //Remove any stale results in the places we searched (since we do not remove potentially stale results
+ //on a text change, we have to remove them here) and then add in the results we found.
+ if (_options.HasFlag(FindOptions.Multiline))
+ {
+ //Multiline searches are always whole buffer searches, so we can skip the set operations.
+ Debug.Assert(requestedSpans.Count == 1);
+ Debug.Assert(requestedSpans[0].Start == 0);
+ Debug.Assert(requestedSpans[0].Length == snapshot.Length);
+
+ _results = new SearchResults(snapshot,
+ new NormalizedSpanCollection(newMatches),
+ new NormalizedSpanCollection(new Span(0, snapshot.Length)));
+ }
+ else
+ {
+ //Remove the stale results.
+ NormalizedSpanCollection m = NormalizedSpanCollection.Difference(_results.Matches, requestedSpans);
+
+ //Add in the new results.
+ if (newMatches.Count > 0)
+ {
+ m = NormalizedSpanCollection.Union(m, new NormalizedSpanCollection(newMatches));
+ }
+
+ //Save the results
+ _results = new SearchResults(snapshot,
+ m,
+ NormalizedSpanCollection.Union(_results.SearchedSpans, requestedSpans));
+ }
+
+ //We completed the search & updated the results ... have the tagger to raise the appropriate changed event
+ //on the span we just searched.
+ //
+ //We can't raise the tags changed on just the results since we also need to signal that stale results have
+ //been removed.
+ _callback(snapshot, requestedSpans);
+ }
+ else
+ {
+ //The buffer changed so we can't trust the results we just got (the search may not have completed).
+ //Don't dequeue the request and we'll repeat the process (but on the correct snapshot).
+ dequeueRequest = false;
+ }
+ }
+
+ if (dequeueRequest)
+ {
+ lock (_requestQueue)
+ {
+ //Nothing should have moved the request out of the queue.
+ Debug.Assert(object.ReferenceEquals(request, _requestQueue.Peek()));
+
+ _requestQueue.Dequeue();
+
+ if (_requestQueue.Count == 0)
+ {
+ //No more requests are pending, release the worker thread.
+ return;
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ Thread.CurrentThread.Priority = ThreadPriority.Normal;
+ }
+ }
+
+ internal ITextSnapshot AdvanceToCurrentSnapshot()
+ {
+ //We don't need to take a snapshot of the results because the results are only modified on this thread.
+ ITextSnapshot oldSnapshot = _results.Snapshot;
+ ITextSnapshot newSnapshot = _buffer.CurrentSnapshot;
+
+ if (oldSnapshot != newSnapshot)
+ {
+ //The results are all on an old snapshot. We need to project them forward (even though that might cause some stale and incorrect
+ //results).
+ NormalizedSpanCollection newMatches = TextSearchNavigator.TranslateTo(oldSnapshot, _results.Matches, newSnapshot);
+ NormalizedSpanCollection newSearchedSpans = NormalizedSpanCollection.Empty;
+
+ if ((_results.SearchedSpans.Count != 0) && !_options.HasFlag(FindOptions.Multiline))
+ {
+ //Advance our record of the spans that have already been searched to the new snapshot as well.
+ newSearchedSpans = BackgroundSearch<T>.TranslateToAndExtend(oldSnapshot, _results.SearchedSpans, newSnapshot);
+
+ //But remove anything on a TextSnapshotLine that was modified by the change.
+ List<Span> changedSpansOnNewSnapshot = new List<Span>();
+ ITextVersion version = oldSnapshot.Version;
+ while (version != newSnapshot.Version)
+ {
+ foreach (var change in version.Changes)
+ {
+ changedSpansOnNewSnapshot.Add(BackgroundSearch<T>.Extend(newSnapshot, Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeInclusive, change.NewSpan,
+ version.Next, newSnapshot.Version)));
+ }
+
+ version = version.Next;
+ }
+
+ if (changedSpansOnNewSnapshot.Count > 0)
+ {
+ NormalizedSpanCollection changes = new NormalizedSpanCollection(changedSpansOnNewSnapshot);
+
+ //Remove the spans touched by changes from the spans we've searched
+ newSearchedSpans = NormalizedSpanCollection.Difference(newSearchedSpans, changes);
+ }
+ }
+
+ _results = new SearchResults(newSnapshot, newMatches, newSearchedSpans);
+ }
+
+ return newSnapshot;
+ }
+
+ public static NormalizedSpanCollection TranslateToAndExtend(ITextSnapshot currentSnapshot, NormalizedSpanCollection currentSpans, ITextSnapshot targetSnapshot)
+ {
+ if (currentSpans.Count == 0)
+ {
+ return currentSpans;
+ }
+
+ List<Span> spans = new List<Span>(currentSpans.Count);
+ foreach (var s in currentSpans)
+ {
+ spans.Add(BackgroundSearch<T>.Extend(targetSnapshot, Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeNegative,
+ s,
+ currentSnapshot.Version, targetSnapshot.Version)));
+ }
+
+ return new NormalizedSpanCollection(spans);
+ }
+
+ //Grow a snapshot span so that it includes all of the TextSnapshotLines that overlap the span (but always return at least one
+ //complete line).
+ public static Span Extend(ITextSnapshot snapshot, Span span)
+ {
+ ITextSnapshotLine start = snapshot.GetLineFromPosition(span.Start);
+ if (span.End <= start.EndIncludingLineBreak.Position)
+ {
+ //source.End is on the same line (or possibly the start of the next line) ... return just this line.
+ return start.ExtentIncludingLineBreak;
+ }
+ else
+ {
+ ITextSnapshotLine end = snapshot.GetLineFromPosition(span.End);
+
+ //if source.End is at the start of the line, only return up to the start of the line, otherwise
+ //include the entire line).
+ return Span.FromBounds(start.Start,
+ (end.Start.Position == span.End)
+ ? end.Start
+ : end.EndIncludingLineBreak);
+ }
+ }
+
+ /// <summary>
+ /// Simulates a search on the range where the user is performing a series of find next operations, buts aborts quickly
+ /// when either the BackgroundSearch advances to a new snapshot or is disposed.
+ /// </summary>
+ private IList<Span> FindAll(ITextSnapshot snapshot, NormalizedSpanCollection spans)
+ {
+ IList<Span> matches = new List<Span>();
+
+ int start = int.MinValue;
+ int end = int.MinValue;
+
+ foreach (var span in spans)
+ {
+ //All the spans are normalized to conver entire text snapshot lines.
+ Debug.Assert(snapshot.GetLineFromPosition(span.Start).Start == span.Start);
+ Debug.Assert((span.End == snapshot.Length) || (snapshot.GetLineFromPosition(span.End).Start == span.End));
+
+ if (span.Length > 0)
+ {
+ SnapshotSpan searchRange = new SnapshotSpan(snapshot, span);
+ SnapshotPoint startingPosition = searchRange.Start;
+
+ while (true)
+ {
+ if (_isDisposed || (snapshot != _buffer.CurrentSnapshot))
+ {
+ //We've been disposed of or the buffer has advanced to a new snapshot. Either way, abort the search.
+ return matches;
+ }
+
+ SnapshotSpan? match = _textSearchService.Find(searchRange, startingPosition, _searchTerm, _options);
+
+ if (match.HasValue)
+ {
+ if (match.Value.Start > end)
+ {
+ //The current match is disjoint from the last match, add it to the list of matches.
+ if (end != int.MinValue)
+ {
+ matches.Add(Span.FromBounds(start, end));
+
+ //Avoid problems when there are so many matches (e.g. searching for 'a' in a 300MB file) that
+ //we run out of memory tracking results.
+ //
+ //The effect of this cut-out isn't exactly predictable (doing several smaller searches will
+ //allow the total number of results maintained by the background search class to grow past
+ //the limit but a large search will hit the limit and miss results).
+ if (matches.Count > 5000)
+ return matches;
+ }
+
+ start = match.Value.Start;
+ end = match.Value.End;
+ }
+ else
+ {
+ //The new match overlaps the old. Simple extend the existing matched span.
+ end = Math.Max(end, match.Value.End); //With an RE, the new match could end before the end of the previous match.
+ }
+
+ startingPosition = match.Value.Start;
+
+ if (startingPosition >= span.End)
+ {
+ break;
+ }
+ else
+ {
+ startingPosition += 1;
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ if (end != int.MinValue)
+ {
+ matches.Add(Span.FromBounds(start, end));
+ }
+
+ return matches;
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ _isDisposed = true;
+ }
+
+ #endregion
+
+ internal class SearchResults
+ {
+ public readonly ITextSnapshot Snapshot;
+ public readonly NormalizedSpanCollection Matches;
+ public readonly NormalizedSpanCollection SearchedSpans;
+
+ public SearchResults(ITextSnapshot snapshot, NormalizedSpanCollection matches, NormalizedSpanCollection searchedSpans)
+ {
+ this.Snapshot = snapshot;
+ this.Matches = matches;
+ this.SearchedSpans = searchedSpans;
+ }
+ }
+ }
+}
diff --git a/src/Text/Impl/TextSearch/TextSearchNavigator.cs b/src/Text/Impl/TextSearch/TextSearchNavigator.cs
new file mode 100644
index 0000000..360e9e4
--- /dev/null
+++ b/src/Text/Impl/TextSearch/TextSearchNavigator.cs
@@ -0,0 +1,521 @@
+//
+// 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.Find.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ class TextSearchNavigator : ITextSearchNavigator2
+ {
+ readonly ITextBuffer _buffer;
+ readonly ITextSearchService2 _textSearchService;
+
+ public TextSearchNavigator(ITextSearchService2 textSearchService, ITextBuffer buffer)
+ {
+ _buffer = buffer;
+ _textSearchService = textSearchService;
+ }
+
+ #region Private Helpers
+
+ /// <summary>
+ /// Calculates the search start point for the next search operation. Always returns a point on the buffer's
+ /// current snapshot. If no point is returned, then we're at ends of the buffer (or search range)
+ /// and wrap is turned off.
+ /// </summary>
+ private SnapshotPoint? CalculateStartPoint(ITextSnapshot searchSnapshot, bool wrap, bool forward)
+ {
+ // If there is a current result, then use its span to figure out the next starting point, otherwise
+ // use the StartPoint itself
+ SnapshotSpan? currentResult = this.CurrentResult;
+ SnapshotPoint nextSearchStart;
+
+ if (currentResult.HasValue)
+ {
+ int position;
+ if (forward)
+ {
+ // moving forwards (by default one more than the start of the previous match).
+ position = currentResult.Value.Start.Position + 1;
+
+ if (position > searchSnapshot.Length)
+ {
+ // The previous search result was at the end of the buffer
+ if (wrap)
+ {
+ position = 0;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ else
+ {
+ // Moving backwards (by default 1 less than the end of the previous match).
+ position = currentResult.Value.End.Position - 1;
+ if (position < 0)
+ {
+ // The last position was at the start of the buffer
+ if (wrap)
+ {
+ position = searchSnapshot.Length;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ nextSearchStart = new SnapshotPoint(currentResult.Value.Snapshot, position);
+ }
+ else if (this.StartPoint != null)
+ {
+ // We have no current result, simply use the starting point as the search point.
+ // If there is none, then use the start of the buffer as the search starting point.
+ nextSearchStart = this.StartPoint.Value;
+ }
+ else
+ {
+ //If all else fails, start at the start of the buffer.
+ nextSearchStart = new SnapshotPoint(_buffer.CurrentSnapshot, 0);
+ }
+
+ return nextSearchStart.TranslateTo(searchSnapshot, GetTrackingMode(forward));
+ }
+ #endregion
+
+ #region ITextSearchNavigator Members
+ public string SearchTerm { get; set; }
+
+ public string ReplaceTerm { get; set; }
+
+ public FindOptions SearchOptions { get; set; }
+
+ public SnapshotSpan? CurrentResult { get; private set; }
+
+ private ITrackingSpan _searchSpan;
+ public ITrackingSpan SearchSpan
+ {
+ get
+ {
+ var span = _searchSpan;
+ if ((span == null) && (_searchSpans != null) && (_searchSpans.Count > 0))
+ {
+ var snapshot = _searchSpans[0].Snapshot;
+ span = snapshot.CreateTrackingSpan(Span.FromBounds(_searchSpans[0].Start, _searchSpans[_searchSpans.Count - 1].End), SpanTrackingMode.EdgeInclusive);
+ }
+ return span;
+ }
+ set
+ {
+ if (value != null && value.TextBuffer != _buffer)
+ {
+ throw new InvalidOperationException("The SearchSpan must be on the same buffer as the navigator itself.");
+ }
+
+ //We keep _searchSpan & _searchSpans (instead of converting this to a NormalizedSnapshotSpanCollection) because the user
+ //could have provided a custom tracking span and we'd loose the tracking behavior in the conversion.
+ _searchSpan = value;
+ _searchSpans = null;
+ }
+ }
+
+ private SnapshotPoint? _startPoint;
+ public SnapshotPoint? StartPoint
+ {
+ get
+ {
+ UpdateStartPoint();
+ return _startPoint;
+ }
+ set
+ {
+ if (value != null && value.Value.Snapshot.TextBuffer != _buffer)
+ {
+ throw new ArgumentException("StartPoint must be on the same buffer as the search navigator itself.");
+ }
+
+ _startPoint = value;
+ }
+ }
+
+ public bool Find()
+ {
+ if (string.IsNullOrEmpty(this.SearchTerm))
+ {
+ throw new InvalidOperationException("You must set a non-empty search term before searching.");
+ }
+
+ bool forward = (this.SearchOptions & FindOptions.SearchReverse) != FindOptions.SearchReverse;
+ bool wrap = (this.SearchOptions & FindOptions.Wrap) == FindOptions.Wrap;
+ bool regEx = (this.SearchOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions;
+
+ ITextSnapshot searchSnapshot = _buffer.CurrentSnapshot;
+
+ //There could be a version skew here if someone calls find from inside a text changed callback on the buffer. That probably wouldn't be a good
+ //idea but we need to handle it gracefully.
+ this.AdvanceToSnapshot(searchSnapshot);
+
+ SnapshotPoint? searchStart = this.CalculateStartPoint(searchSnapshot, wrap, forward);
+ if (searchStart.HasValue)
+ {
+ int index = 0;
+ NormalizedSnapshotSpanCollection searchSpans = this.SearchSpans;
+
+ if (searchSpans != null)
+ {
+ Debug.Assert(searchSpans.Count > 0);
+
+ //Index is potentially outside the range of [0...searchSpans.Count-1] but we handle that below.
+ if (!(TextSearchNavigator.TryGetIndexOfContainingSpan(searchSpans, searchStart.Value, out index) || forward))
+ {
+ //For reversed searches, we want the index of the span before the point if we can't get a span that contains the point.
+ --index;
+ }
+ }
+ else
+ {
+ searchSpans = new NormalizedSnapshotSpanCollection(new SnapshotSpan(searchSnapshot, Span.FromBounds(0, searchSnapshot.Length)));
+ }
+
+ int searchIterations = searchSpans.Count;
+ for (int i = 0; (i < searchIterations); ++i)
+ {
+ //index needs to be normalized to [0 ... searchSpans.Count - 1] but could be negative.
+ index = (index + searchSpans.Count) % searchSpans.Count;
+
+ SnapshotSpan searchSpan = searchSpans[index];
+ if ((i != 0) || (searchStart.Value < searchSpan.Start) || (searchStart.Value > searchSpan.End))
+ {
+ searchStart = forward ? searchSpan.Start : searchSpan.End;
+ }
+ else if (wrap && (i == 0))
+ {
+ //We will need to repeat the search to account for wrap being on and we are not searching everything in searchSpans[0].
+ //This is the same as simply doing a search for i == searchSpans.Count we we can make happen by bumping the number of iterations.
+ ++searchIterations;
+ }
+
+ foreach (var result in _textSearchService.FindAll(searchSpan, searchStart.Value, this.SearchTerm, this.SearchOptions & ~FindOptions.Wrap))
+ {
+ // As a safety measure, we don't include results of length zero in the navigator unless regular expressions are being used.
+ // Zero width matches could be useful in RegEx when for example somebody is trying to replace the start of the line using the "^"
+ // pattern.
+ if (result.Length == 0 && !regEx)
+ {
+ continue;
+ }
+ else
+ {
+ // We accept the first match
+ this.CurrentResult = result;
+ return true;
+ }
+ }
+
+ if (forward)
+ {
+ ++index;
+ }
+ else
+ {
+ --index;
+ }
+ }
+ }
+
+ // If nothing was found, then clear the current result
+ this.ClearCurrentResult();
+
+ return false;
+ }
+
+ public bool Replace()
+ {
+ if (this.ReplaceTerm == null)
+ {
+ throw new InvalidOperationException("Can't replace with a null value. Set ReplaceTerm before performing a replace operation.");
+ }
+
+ if (!this.CurrentResult.HasValue)
+ {
+ throw new InvalidOperationException("Need to have a current result before being able to replace. Perform a FindNext or FindPrevious operation first.");
+ }
+
+ bool forward = (this.SearchOptions & FindOptions.SearchReverse) != FindOptions.SearchReverse;
+ bool regEx = (this.SearchOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions;
+
+ //This may not be the text buffer's current snapshot but that is the desired behavior. We're replacing the current result
+ //with the replace tuern.
+ SnapshotSpan result = this.CurrentResult.Value;
+ ITextSnapshot replaceSnapshot = result.Snapshot;
+
+ SnapshotPoint searchStart = forward ? result.Start : result.End;
+
+ SnapshotSpan searchSpan;
+ NormalizedSnapshotSpanCollection searchSpans = this.SearchSpans;
+ if ((searchSpans != null) && (searchSpans.Count > 0))
+ {
+ //There could be a version skew here.
+ if (searchSpans[0].Snapshot != replaceSnapshot)
+ {
+ searchSpans = new NormalizedSnapshotSpanCollection(replaceSnapshot, TextSearchNavigator.TranslateTo(searchSpans[0].Snapshot, searchSpans, replaceSnapshot));
+ }
+
+ int index;
+ if (!TextSearchNavigator.TryGetIndexOfContainingSpan(searchSpans, searchStart, out index))
+ {
+ // If the match is outside of the search range, then we should noop
+ return false;
+ }
+ searchSpan = searchSpans[index];
+ }
+ else
+ {
+ searchSpan = new SnapshotSpan(replaceSnapshot, 0, replaceSnapshot.Length);
+ }
+
+ searchSpan = forward ? new SnapshotSpan(searchStart, searchSpan.End) : new SnapshotSpan(searchSpan.Start, searchStart);
+
+ //Ask the search engine to find the actual span we need to replace (& the corresponding replacement string).
+ string replacementValue = null;
+ SnapshotSpan? toReplace = _textSearchService.FindForReplace(searchSpan, this.SearchTerm, this.ReplaceTerm, this.SearchOptions, out replacementValue);
+
+ if (toReplace.HasValue)
+ {
+ using (ITextEdit edit = _buffer.CreateEdit())
+ {
+ Span replacementSpan = toReplace.Value.TranslateTo(edit.Snapshot, SpanTrackingMode.EdgeInclusive);
+
+ if (!edit.Replace(replacementSpan, replacementValue))
+ {
+ // The edit failed for some reason, perhaps read-only regions?
+ return false;
+ }
+
+ edit.Apply();
+
+ if (edit.Canceled)
+ {
+ // The edit failed, most likely a handler of the changed event forced the edit to be canceled.
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void ClearCurrentResult()
+ {
+ this.CurrentResult = null;
+ }
+
+ /// <summary>
+ /// Translate start point to current snapshot
+ /// </summary>
+ private void UpdateStartPoint()
+ {
+ if (_startPoint.HasValue && _startPoint.Value.Snapshot != _buffer.CurrentSnapshot)
+ {
+ ITextVersion currentVersion = _startPoint.Value.Snapshot.Version;
+ ITextVersion targetVersion = _buffer.CurrentSnapshot.Version;
+ bool reverse = this.SearchOptions.HasFlag(Text.Operations.FindOptions.SearchReverse);
+ int currentStartPointPosition = _startPoint.Value.Position;
+
+ Debug.Assert(targetVersion.VersionNumber >= currentVersion.VersionNumber, "We should never have to translate StartPoint into past");
+
+ while (currentVersion != targetVersion)
+ {
+ // Per INormalizedTextChangeCollection contract, there is not more than one change that we need to check. Find it with binary search
+ int changeCount = currentVersion.Changes.Count;
+ int lo = 0;
+ int hi = changeCount - 1;
+
+ while (lo <= hi)
+ {
+ int mid = (lo + hi) / 2;
+ ITextChange textChange = currentVersion.Changes[mid];
+
+ if (currentStartPointPosition < textChange.OldPosition)
+ {
+ hi = mid - 1;
+ }
+ else if (currentStartPointPosition > textChange.OldEnd)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ // Found the change. Let's adjust currentStartPointPosition
+ if (reverse)
+ {
+ // Partially verified by binary search. The full condition is in assert:
+ Debug.Assert(textChange.OldSpan.Start <= currentStartPointPosition && currentStartPointPosition <= textChange.OldSpan.End);
+
+ if (currentStartPointPosition < textChange.OldSpan.End && !textChange.NewSpan.IsEmpty)
+ {
+ currentStartPointPosition = textChange.NewSpan.End - 1;
+ }
+ else // currentStartPosition == textChange.OldSpan.End
+ {
+ currentStartPointPosition = textChange.NewSpan.End;
+ }
+ }
+ else
+ {
+ if (textChange.OldSpan.End == currentStartPointPosition)
+ {
+ currentStartPointPosition = textChange.NewSpan.End;
+ }
+ else if (textChange.OldSpan.Start < currentStartPointPosition && !textChange.NewSpan.IsEmpty)
+ {
+ currentStartPointPosition = textChange.NewSpan.End - 1;
+ }
+ else
+ {
+ currentStartPointPosition = textChange.NewSpan.Start;
+ }
+ }
+ break;
+ }
+ }
+
+ if (hi < lo)
+ {
+ Debug.Assert(hi == lo - 1, "If we haven't found a change, hi should be equal to lo - 1");
+
+ if (lo > 0) // Current position lies between the changes
+ {
+ ITextChange textChange = currentVersion.Changes[lo - 1];
+ currentStartPointPosition = currentStartPointPosition + (textChange.NewEnd - textChange.OldEnd);
+ }
+ // else the start point lays prior to the first change (or there are no changes) and should remain intact
+ }
+
+ currentVersion = currentVersion.Next;
+ }
+
+ _startPoint = new SnapshotPoint(_buffer.CurrentSnapshot, currentStartPointPosition);
+ }
+ }
+
+ static PointTrackingMode GetTrackingMode(bool isReverse)
+ {
+ return isReverse ? PointTrackingMode.Positive : PointTrackingMode.Negative;
+ }
+ #endregion
+
+ #region ITextSearchNavigator2 Members
+ private NormalizedSnapshotSpanCollection _searchSpans;
+ public NormalizedSnapshotSpanCollection SearchSpans
+ {
+ get
+ {
+ var spans = _searchSpans;
+ if ((spans == null) && (_searchSpan != null))
+ {
+ var snapshot = _buffer.CurrentSnapshot;
+ spans = new NormalizedSnapshotSpanCollection(new SnapshotSpan(snapshot, _searchSpan.GetSpan(snapshot)));
+ }
+ return spans;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ if (value.Count == 0)
+ {
+ //Treat an empty collection as if it were null.
+ value = null;
+ }
+ else if (value[0].Snapshot.TextBuffer != _buffer)
+ {
+ throw new InvalidOperationException("The SearchSpan must be on the same buffer as the navigator itself.");
+ }
+ }
+
+ _searchSpans = value;
+ _searchSpan = null;
+ }
+ }
+ #endregion
+
+ private void AdvanceToSnapshot(ITextSnapshot snapshot)
+ {
+ if ((_searchSpans != null) && (_searchSpans.Count > 0) && (_searchSpans[0].Snapshot != snapshot))
+ {
+ NormalizedSpanCollection newSpans = TextSearchNavigator.TranslateTo(_searchSpans[0].Snapshot, _searchSpans, snapshot);
+ this.SearchSpans = new NormalizedSnapshotSpanCollection(snapshot, newSpans);
+ }
+ }
+
+ public static NormalizedSpanCollection TranslateTo(ITextSnapshot currentSnapshot, NormalizedSpanCollection currentSpans, ITextSnapshot targetSnapshot)
+ {
+ if ((currentSpans.Count == 0) || (currentSnapshot == targetSnapshot))
+ {
+ return currentSpans;
+ }
+
+ bool forwardInTime = (currentSnapshot.Version.VersionNumber < targetSnapshot.Version.VersionNumber);
+
+ return new NormalizedSnapshotSpanCollection(targetSnapshot,
+ currentSpans.Select(s =>
+ forwardInTime
+ ? Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeInclusive,
+ s,
+ currentSnapshot.Version, targetSnapshot.Version)
+ : Tracking.TrackSpanBackwardInTime(SpanTrackingMode.EdgeInclusive,
+ s,
+ currentSnapshot.Version, targetSnapshot.Version)).Where(s => s.Length != 0));
+ }
+
+ /// <summary>
+ /// Search spans from [start ... spans.Count-1] for a span that contains point.
+ /// </summary>
+ private static bool TryGetIndexOfContainingSpan(NormalizedSpanCollection spans, int point, out int index)
+ {
+ int lo = 0;
+ int hi = spans.Count;
+ while (lo < hi)
+ {
+ int mid = (lo + hi) / 2;
+ Span s = spans[mid];
+
+ if (s.End < point)
+ {
+ lo = mid + 1;
+ }
+ else if (s.Start > point)
+ {
+ hi = mid;
+ }
+ else
+ {
+ //We know s.Start <= point <= s.End
+ index = mid;
+ return true;
+ }
+ }
+
+ //None of the spans contains point. lo is the index of the span that follows point
+ index = lo;
+ return false;
+ }
+ }
+}
diff --git a/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs
new file mode 100644
index 0000000..278532f
--- /dev/null
+++ b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs
@@ -0,0 +1,37 @@
+//
+// 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.Find.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Operations;
+
+ [Export(typeof(ITextSearchNavigatorFactoryService))]
+ class TextSearchNavigatorFactoryService : ITextSearchNavigatorFactoryService
+ {
+ [Import]
+ ITextSearchService2 TextSearchService = null;
+
+ #region ITextSearchNavigatorFactoryService Members
+
+ public ITextSearchNavigator CreateSearchNavigator(ITextBuffer buffer)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ // Don't return a singleton since it's allowed to have multiple search navigators on the same buffer
+ return new TextSearchNavigator(this.TextSearchService, buffer);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextSearch/TextSearchService.cs b/src/Text/Impl/TextSearch/TextSearchService.cs
new file mode 100644
index 0000000..71c44e1
--- /dev/null
+++ b/src/Text/Impl/TextSearch/TextSearchService.cs
@@ -0,0 +1,751 @@
+//
+// 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.Find.Implementation
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.Text.RegularExpressions;
+ using Microsoft.VisualStudio.Text.Operations;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using System.Linq;
+ using System.Threading;
+
+ [Export(typeof(ITextSearchService))]
+ [Export(typeof(ITextSearchService2))]
+ // Ensure only a singleton instance is used for all operations
+ [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)]
+ internal partial class TextSearchService : ITextSearchService2
+ {
+ [Import]
+ ITextStructureNavigatorSelectorService _navigatorSelectorService;
+
+ // Cache of recently used Regex expressions to save on construction
+ // of Regex objects.
+ static IDictionary<string, WeakReference> _cachedRegexEngines;
+ static ReaderWriterLockSlim _regexCacheLock;
+
+ // Maximum number of Regex engines to cache
+ const int _maxCachedRegexEngines = 10;
+
+ static TextSearchService()
+ {
+ _cachedRegexEngines = new Dictionary<string, WeakReference>(_maxCachedRegexEngines);
+ _regexCacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
+ }
+
+ #region ITextSearchService Members
+
+ public SnapshotSpan? FindNext(int startIndex, bool wraparound, FindData findData)
+ {
+ // We allow startIndex to be at the end of the buffer
+ if ((startIndex < 0) || (startIndex > findData.TextSnapshotToSearch.Length))
+ {
+ throw new ArgumentOutOfRangeException("startIndex");
+ }
+
+ if (string.IsNullOrEmpty(findData.SearchString))
+ {
+ throw new ArgumentException("Search pattern can't be empty or null", "findData");
+ }
+
+ FindOptions options = findData.FindOptions;
+
+ if (wraparound)
+ {
+ options |= FindOptions.Wrap;
+ }
+
+ if ((findData.FindOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions)
+ {
+ // Validate regular expression
+ GetRegularExpressionMatches(options, findData.SearchString, string.Empty);
+ }
+
+ bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord;
+
+ // We want to wrap if either the boolean parameter is true or if the option is set via
+ // FindOptions
+ wraparound |= (findData.FindOptions & FindOptions.Wrap) == FindOptions.Wrap;
+
+ ITextSnapshot snapshot = findData.TextSnapshotToSearch;
+
+ // Remove whole word option, even if set so that new implementation's whole word logic is not used.
+ // Instead we use our own legacy whole word logic here to match the old behavior and preserve
+ // backward compatibility.
+ foreach (Tuple<SnapshotSpan, string> result in
+ FindAllForReplace(new SnapshotPoint(snapshot, startIndex), new SnapshotSpan(snapshot, Span.FromBounds(0, snapshot.Length)),
+ findData.SearchString, null, options & ~FindOptions.WholeWord))
+ {
+ // In case whole word option was requested and the result is not a whole word, then
+ // keep looking.
+ if (wholeWord && !this.LegacyMatchesAWholeWord(result.Item1, findData))
+ {
+ continue;
+ }
+
+ return result.Item1;
+ }
+
+ return null;
+ }
+
+ public Collection<SnapshotSpan> FindAll(FindData findData)
+ {
+ if (string.IsNullOrEmpty(findData.SearchString))
+ {
+ throw new ArgumentException("Search pattern can't be empty or null", "findData");
+ }
+
+ FindOptions options = findData.FindOptions;
+
+ bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord;
+
+ ITextSnapshot snapshot = findData.TextSnapshotToSearch;
+ SnapshotSpan searchRange = new SnapshotSpan(snapshot, Span.FromBounds(0, snapshot.Length));
+
+ Collection<SnapshotSpan> results = new Collection<SnapshotSpan>();
+
+ // Remove whole word option, even if set so that new implementation's whole word logic is not used.
+ // Instead we use our own legacy whole word logic here to match the old behavior and preserve
+ // backward compatibility.
+ foreach (SnapshotSpan result in this.FindAll(searchRange, findData.SearchString, options & ~FindOptions.WholeWord))
+ {
+ // In case whole word option was requested and the result is not a whole word, then
+ // keep looking.
+ if (wholeWord && !this.LegacyMatchesAWholeWord(result, findData))
+ {
+ continue;
+ }
+
+ results.Add(result);
+ }
+
+ return results;
+ }
+
+ #endregion
+
+ #region ITextSearchService2 Members
+
+ public SnapshotSpan? Find(SnapshotPoint startingPosition, string searchPattern, FindOptions options)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ return Find(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)), searchPattern, options);
+ }
+
+ public SnapshotSpan? Find(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ if (searchRange.Snapshot != startingPosition.Snapshot)
+ {
+ throw new ArgumentException("The search range and search starting position must belong to the same snapshot.");
+ }
+
+ if (!ContainedBySpan(searchRange, startingPosition))
+ {
+ throw new ArgumentException("The search start point must be contained by the search range.");
+ }
+
+ return Find(startingPosition, searchRange, searchPattern, options);
+ }
+
+ public SnapshotSpan? FindForReplace(SnapshotPoint startingPosition, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ if (replacePattern == null)
+ {
+ throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern");
+ }
+
+ return FindForReplace(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)),
+ searchPattern, replacePattern, options, out expandedReplacePattern);
+ }
+
+ public SnapshotSpan? FindForReplace(SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ if (replacePattern == null)
+ {
+ throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern");
+ }
+
+ return FindForReplace(((options & FindOptions.SearchReverse) != FindOptions.SearchReverse) ? searchRange.Start : searchRange.End, searchRange, searchPattern, replacePattern, options, out expandedReplacePattern);
+ }
+
+ public IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, string searchPattern, FindOptions options)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ if (searchRange.Length == 0)
+ {
+ return new SnapshotSpan[] { };
+ }
+
+ return FindAllForReplace(searchRange.Start, searchRange, searchPattern, null, options).Select(r => r.Item1);
+ }
+
+ public IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Pattern can't be empty or null", "searchPattern");
+ }
+
+ if (searchRange.Length == 0)
+ {
+ return new SnapshotSpan[] { };
+ }
+
+ if (searchRange.Snapshot != startingPosition.Snapshot)
+ {
+ throw new ArgumentException("searchRange and startingPosition parameters must belong to the same snapshot.");
+ }
+
+ if (!ContainedBySpan(searchRange, startingPosition))
+ {
+ throw new InvalidOperationException("Can't perform a search when the startingPosition is not contained by the searchRange.");
+ }
+
+ return FindAllForReplace(startingPosition, searchRange, searchPattern, null, options).Select(r => r.Item1);
+ }
+
+ public IEnumerable<Tuple<SnapshotSpan, string>> FindAllForReplace(SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options)
+ {
+ if (string.IsNullOrEmpty(searchPattern))
+ {
+ throw new ArgumentException("Search pattern can't be null or empty.", "searchPattern");
+ }
+
+ if (replacePattern == null)
+ {
+ throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern");
+ }
+
+ return FindAllForReplace(searchRange.Start, searchRange, searchPattern, replacePattern, options);
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private static SnapshotSpan? Find(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, FindOptions options)
+ {
+ // Perform a find all and return the first result if any is available. Note that find all is an enumerator and won't search the
+ // entire range unless it is necessary.
+ Tuple<SnapshotSpan, string> match = FindAllForReplace(startPosition, searchRange, searchPattern, null, options).FirstOrDefault();
+
+ if (match != null)
+ {
+ return match.Item1;
+ }
+
+ return null;
+ }
+
+ private static SnapshotSpan? FindForReplace(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern)
+ {
+ // Set this value to empty to adhere to the contract (i.e. when no matches are found, this value should be the empty string)
+ expandedReplacePattern = string.Empty;
+
+ // Perform a find all for replace over the range of interest. Note that this operation is lazy and will stop when the first result is
+ // found.
+ Tuple<SnapshotSpan, string> match = FindAllForReplace(startPosition, searchRange, searchPattern, replacePattern, options).FirstOrDefault();
+
+ if (match != null)
+ {
+ expandedReplacePattern = match.Item2;
+ return match.Item1;
+ }
+
+ return null;
+ }
+
+ private static IEnumerable<Tuple<SnapshotSpan, string>> FindAllForReplace(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options)
+ {
+ bool multiLine = (options & FindOptions.Multiline) == FindOptions.Multiline;
+ bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord;
+
+ IEnumerable<Tuple<SnapshotSpan, string>> searchResults = null;
+
+ // Perform the search depending on whether we are forced to perform multi-line search
+ if (multiLine)
+ {
+ searchResults = FindMultiline(startPosition, searchRange, options, searchPattern, replacePattern);
+ }
+ else
+ {
+ searchResults = FindSingleLine(startPosition, searchRange, options, searchPattern, replacePattern);
+ }
+
+ foreach (Tuple<SnapshotSpan, string> searchResult in searchResults)
+ {
+ // Don't accept the result if whole word option is selected and the result
+ // is not a whole word
+ if (wholeWord && !IsWholeWord(searchResult.Item1))
+ {
+ continue;
+ }
+
+ // found a suitable match, return it!
+ yield return searchResult;
+ }
+ }
+
+ private static IEnumerable<Tuple<SnapshotSpan, string>> FindMultiline(SnapshotPoint startPosition, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern = null)
+ {
+ Debug.Assert(searchRange.Contains(startPosition) || searchRange.End == startPosition);
+
+ // This is pretty disgusting since we have to search including line endings
+ // we have to look at the entire string. There can be optimizations done for specific cases
+ // where one can count the number of line endings that a search pattern could at most include
+ // and then search chunks with that many line endings at a time, but for patterns where the quantifier
+ // is * this can't be done; e.g. a.*[\n]*.*b
+ string rangeText = searchRange.GetText();
+
+ bool wrap = (options & FindOptions.Wrap) == FindOptions.Wrap;
+
+ foreach (var result in FindInString(searchRange.Start, startPosition - searchRange.Start, rangeText, options, searchPattern, replacePattern))
+ {
+ yield return result;
+ }
+
+ if (wrap)
+ {
+ bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse;
+
+ foreach (var result in FindInString(searchRange.Start, reverse ? rangeText.Length : 0, rangeText, options, searchPattern, replacePattern))
+ {
+ // Since we are wrapping, check the validity of the result to ensure it was not returned
+ // in our first search above
+ if (reverse)
+ {
+ if (result.Item1.End <= startPosition)
+ {
+ break;
+ }
+ }
+ else
+ {
+ if (result.Item1.Start >= startPosition)
+ {
+ break;
+ }
+ }
+
+ yield return result;
+ }
+ }
+ }
+
+ private static IEnumerable<Tuple<SnapshotSpan, string>> FindSingleLine(SnapshotPoint startPosition, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern = null)
+ {
+ Debug.Assert(searchRange.Contains(startPosition) || searchRange.End == startPosition);
+
+ bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse;
+ bool regEx = (options & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions;
+ bool wrap = (options & FindOptions.Wrap) == FindOptions.Wrap;
+
+ ITextSnapshot snapshot = startPosition.Snapshot;
+ ITextSnapshotLine startLine = startPosition.GetContainingLine();
+ int lineIncrement = reverse ? -1 : 1;
+ int startLineNumber = startLine.LineNumber;
+ int searchLineStart = searchRange.Start.GetContainingLine().LineNumber;
+ int searchLineEnd = searchRange.End.GetContainingLine().LineNumber;
+
+ SnapshotSpan firstLineSpan = startLine.ExtentIncludingLineBreak.Intersection(searchRange).Value;
+
+ // Find results in "first part" of the first line, then search the rest of the range. If wrap is on, afterwards wrap and search
+ // the rest of the range, then finally check the first line again and search its "second part".
+
+ // We are effecitvely splitting the first line into the following two portions:
+ //
+ // FirstLineStart --------------------- startPosition --------------------- FirstLineEnd
+ //
+ // Depending on the direction of the search, one of the pieces will be searched first, and the other will be searched if
+ // wrap is on towards the bottom of the algorithm after all other lines have been searched.
+
+ // Examine first line (regEx can match 0 length strings so we can't exclude that case).
+ if ((firstLineSpan.Length > 0) || regEx)
+ {
+ // Regular expression searches start from the index passed - 1 in reverse direction, as such, if we are at a line boundary
+ // we need to be examining the line before
+ bool skip = regEx && reverse && startPosition != 0 && startLine.Start == startPosition;
+
+ if (!skip)
+ {
+ foreach (var result in FindInString(firstLineSpan.Start, startPosition - firstLineSpan.Start, firstLineSpan.GetText(), options, searchPattern, replacePattern))
+ {
+ yield return result;
+ }
+ }
+ }
+
+ // Examine the rest of the range in the original direction
+ for (int i = startLineNumber + lineIncrement; (i >= searchLineStart && i <= searchLineEnd); i += lineIncrement)
+ {
+ foreach (var result in FindInLine(i, searchRange, options, searchPattern, replacePattern))
+ {
+ yield return result;
+ }
+ }
+
+ // If wrap is enabled, now wrap around the search boundary and continue the search
+ if (wrap)
+ {
+ for (int i = reverse ? searchLineEnd : searchLineStart; i != startLineNumber; i += lineIncrement)
+ {
+ foreach (var result in FindInLine(i, searchRange, options, searchPattern, replacePattern))
+ {
+ yield return result;
+ }
+ }
+
+ // Finally look at the remainder of the first line
+ foreach (var result in FindInString(firstLineSpan.Start, reverse ? firstLineSpan.End - firstLineSpan.Start : 0,
+ firstLineSpan.GetText(), options, searchPattern, replacePattern))
+ {
+ // Since we are wrapping, check the validity of the result to ensure it was not returned
+ // in our first search of the first line before wrapping
+ if (reverse)
+ {
+ if (result.Item1.End.Position <= startPosition.Position)
+ {
+ break;
+ }
+ }
+ else
+ {
+ if (result.Item1.Start >= startPosition)
+ {
+ break;
+ }
+ }
+
+ yield return result;
+ }
+ }
+ }
+
+ private static IEnumerable<Tuple<SnapshotSpan, string>> FindInLine(int lineNumber, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern)
+ {
+ bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse;
+ ITextSnapshotLine line = searchRange.Snapshot.GetLineFromLineNumber(lineNumber);
+ SnapshotSpan? searchSpan = searchRange.Intersection(line.ExtentIncludingLineBreak);
+
+ if (searchSpan.HasValue)
+ {
+ foreach (var result in FindInString(searchSpan.Value.Start, reverse ? searchSpan.Value.End - searchSpan.Value.Start : 0, searchSpan.Value.GetText(), options, searchPattern, replacePattern))
+ {
+ yield return result;
+ }
+ }
+ }
+
+ private static bool IsWholeWord(SnapshotSpan result)
+ {
+ if (result.Length < 1)
+ {
+ return false;
+ }
+
+ return UnicodeWordExtent.IsWholeWord(result);
+ }
+
+ private static bool ContainedBySpan(SnapshotSpan searchRange, SnapshotPoint startingPosition)
+ {
+ // We make an exception for spans to contain points if the end point of the span matches the startingPosition.
+ // For backwards searches, this allows the consumer to find a point at the very end of the searchRange, for
+ // forward searches, this has no effect.
+ return searchRange.Contains(startingPosition) || searchRange.End == startingPosition;
+ }
+
+ private static StringComparison GetStringComparison(FindOptions findOptions)
+ {
+ bool matchCase = ((findOptions & FindOptions.MatchCase) == FindOptions.MatchCase);
+ bool cultureSensitive = ((findOptions & FindOptions.OrdinalComparison) != FindOptions.OrdinalComparison);
+
+ if (cultureSensitive)
+ {
+ if (matchCase)
+ {
+ return StringComparison.CurrentCulture;
+ }
+ else
+ {
+ return StringComparison.CurrentCultureIgnoreCase;
+ }
+ }
+ else
+ {
+ if (matchCase)
+ {
+ return StringComparison.Ordinal;
+ }
+ else
+ {
+ return StringComparison.OrdinalIgnoreCase;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns collection of matches of a regular expression search over the provided input string.
+ /// </summary>
+ private static MatchCollection GetRegularExpressionMatches(FindOptions options, string searchTerm, string toSearch, int startingIndex = -1)
+ {
+ RegexOptions regExOptions = GetRegexOptions(options);
+
+ try
+ {
+ Regex engine = GetOrCreateCachedRegex(regExOptions, searchTerm);
+
+ if (startingIndex == -1)
+ {
+ return engine.Matches(toSearch);
+ }
+ else
+ {
+ return engine.Matches(toSearch, startingIndex);
+ }
+ }
+ catch (ArgumentException e)
+ {
+ throw new ArgumentException("Invalid regular expression", e);
+ }
+ }
+
+ private static Regex GetOrCreateCachedRegex(RegexOptions options, string searchTerm)
+ {
+ // Try to reduce the cache entries if need be
+ ScavengeRegexCache();
+
+ string key = GetRegexKey(options, searchTerm);
+ Regex result = null;
+
+ try
+ {
+ _regexCacheLock.EnterReadLock();
+
+ WeakReference regexReference = null;
+ if (_cachedRegexEngines.TryGetValue(key, out regexReference) && regexReference.IsAlive)
+ {
+ result = regexReference.Target as Regex;
+ }
+ }
+ finally
+ {
+ _regexCacheLock.ExitReadLock();
+ }
+
+ if (result == null)
+ {
+ // Either the key was not found, or it was found and the reference is dead
+ result = new Regex(searchTerm, options);
+
+ try
+ {
+ _regexCacheLock.EnterWriteLock();
+
+ // Cache the value
+ _cachedRegexEngines[key] = new WeakReference(result);
+ }
+ finally
+ {
+ _regexCacheLock.ExitWriteLock();
+ }
+ }
+
+ return result;
+ }
+
+ private static void ScavengeRegexCache()
+ {
+ // If we have reached our maximum capacity, do a pass and kill inactive references
+ if (_maxCachedRegexEngines == _cachedRegexEngines.Count)
+ {
+ try
+ {
+ _regexCacheLock.EnterWriteLock();
+
+ // Keep a list of stale keys
+ string[] staleKeys = new string[_maxCachedRegexEngines];
+
+ int staleKeyIterator = 0;
+
+ // Record all stale keys
+ foreach (var keyValuePair in _cachedRegexEngines)
+ {
+ if (!keyValuePair.Value.IsAlive)
+ {
+ staleKeys[staleKeyIterator++] = keyValuePair.Key;
+ }
+ }
+
+ // Remove stale keys
+ for (int i = 0; i < staleKeyIterator; i++)
+ {
+ _cachedRegexEngines.Remove(staleKeys[i]);
+ }
+ }
+ finally
+ {
+ _regexCacheLock.ExitWriteLock();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates a unique key for the combination of regular expression options and the search term used. Logic
+ /// is copied from ndp/fx/src/Regex/System/Text/RegularExpressions/Regex.cs
+ /// </summary>
+ private static string GetRegexKey(RegexOptions options, string searchTerm)
+ {
+ string cultureKey = null;
+
+ // Try to look up this regex in the cache. We do this regardless of whether useCache is true since there's
+ // really no reason not to.
+ if ((options & RegexOptions.CultureInvariant) != 0)
+ cultureKey = System.Globalization.CultureInfo.InvariantCulture.ToString(); // "English (United States)"
+ else
+ cultureKey = System.Globalization.CultureInfo.CurrentCulture.ToString();
+
+ return ((int)options).ToString(System.Globalization.NumberFormatInfo.InvariantInfo) + ":" + cultureKey + ":" + searchTerm;
+ }
+
+ /// <summary>
+ /// Converts regular find options to regular expression options.
+ /// </summary>
+ private static RegexOptions GetRegexOptions(FindOptions options)
+ {
+ RegexOptions regExOptions = RegexOptions.None;
+
+ if ((options & FindOptions.Multiline) == FindOptions.Multiline)
+ {
+ regExOptions |= RegexOptions.Multiline;
+ }
+
+ if ((options & FindOptions.SingleLine) == FindOptions.SingleLine)
+ {
+ regExOptions |= RegexOptions.Singleline;
+ }
+
+ if ((options & FindOptions.MatchCase) != FindOptions.MatchCase)
+ {
+ regExOptions |= RegexOptions.IgnoreCase;
+ }
+
+ if ((options & FindOptions.SearchReverse) == FindOptions.SearchReverse)
+ {
+ regExOptions |= RegexOptions.RightToLeft;
+ }
+
+ return regExOptions;
+ }
+
+ /// <summary>
+ /// Determines whether the given match qualifies as a whole word using <see cref="ITextStructureNavigator"/>.
+ /// </summary>
+ /// <remarks>
+ /// In the new implementation, we don't use any external parties to determine word boundaries, rather we pick a small
+ /// predictable range of characters and use them as word splitters. The primary reason for this is performance as calls
+ /// to external components could be slow. Further, we want the search service to be a dumb but very predictable and simple
+ /// text based search service so we enforce our own rules in the new implementation.
+ /// </remarks>
+ private bool LegacyMatchesAWholeWord(SnapshotSpan result, FindData findData)
+ {
+ ITextStructureNavigator textStructureNavigator = findData.TextStructureNavigator;
+
+ if (textStructureNavigator == null)
+ {
+ // the navigator was never set; create one without benefit of context
+ textStructureNavigator = _navigatorSelectorService.GetTextStructureNavigator(findData.TextSnapshotToSearch.TextBuffer);
+ }
+
+ // We'll need to get the left extent and right extent in case the match spans across multiple words
+ Microsoft.VisualStudio.Text.Operations.TextExtent leftExtent = textStructureNavigator.GetExtentOfWord(result.Start);
+ Microsoft.VisualStudio.Text.Operations.TextExtent rightExtent = textStructureNavigator.GetExtentOfWord(result.Length > 0 ? result.End - 1 : result.End);
+
+ return (result.Start == leftExtent.Span.Start) && (result.End == rightExtent.Span.End);
+ }
+
+ private static IEnumerable<Tuple<SnapshotSpan, string>> FindInString(SnapshotPoint snapshotOffset, int searchStartIndex, string textData, FindOptions options, string searchTerm, string replaceTerm = null)
+ {
+ bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse;
+ bool regEx = (options & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions;
+
+ if (regEx)
+ {
+ // Perform search with regular expressions
+ foreach (Match match in GetRegularExpressionMatches(options, searchTerm, textData, searchStartIndex))
+ {
+ SnapshotSpan matchSpan = new SnapshotSpan(snapshotOffset.Snapshot, new Span(snapshotOffset + match.Index, match.Length));
+ string replacementValue = replaceTerm != null ? match.Result(replaceTerm) : null;
+
+ yield return Tuple.Create<SnapshotSpan, string>(matchSpan, replacementValue);
+ }
+ }
+ else
+ {
+ // Perform raw string search
+ while ((reverse && searchStartIndex > 0) || (!reverse && searchStartIndex < textData.Length))
+ {
+ // Look for the next match
+ // Note: LastIndexOf performs a search inclusive of the position passed to it, but we want reverse searches
+ // to be exclusive of the start position.
+ int matchIndex = reverse ?
+ textData.LastIndexOf(searchTerm, searchStartIndex - 1, GetStringComparison(options)) :
+ textData.IndexOf(searchTerm, searchStartIndex, GetStringComparison(options));
+
+ if (matchIndex == -1)
+ {
+ // Couldn't find any more results
+ yield break;
+ }
+
+ SnapshotSpan potentialResult = new SnapshotSpan(snapshotOffset.Snapshot, snapshotOffset + matchIndex, searchTerm.Length);
+
+ yield return Tuple.Create<SnapshotSpan, string>(potentialResult, replaceTerm);
+
+ // Prepare for next search
+ //
+ // If the search is forward, we want to continue one character next to the start of the previous result, for instance, consider
+ // searching for "aa" in "aaa".
+ // If the search is backward, we want to continue one character prior to the end of the previous result, again, consider searching
+ // for "aa" in "aaa" from the end of the buffer. Notice that even though the result span does not include its .End position, we
+ // subtract only one, because the search performed above is not inclusive of the start index for reverse direction
+ searchStartIndex = (reverse ? potentialResult.End.Position - 1 : potentialResult.Start.Position + 1) - snapshotOffset.Position;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextSearch/TextSearchTagger.cs b/src/Text/Impl/TextSearch/TextSearchTagger.cs
new file mode 100644
index 0000000..6a8852b
--- /dev/null
+++ b/src/Text/Impl/TextSearch/TextSearchTagger.cs
@@ -0,0 +1,281 @@
+//
+// 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.Find.Implementation
+{
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Operations;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using System;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// A general tagger that takes a search term and tags all matching occurences of it.
+ /// </summary>
+ /// <remarks>
+ /// This tagger -- like most others -- will not raise a TagsChanged event when the buffer changes.
+ /// </remarks>
+ class TextSearchTagger<T> : ITextSearchTagger<T> where T : ITag
+ {
+ // search service to use for doing the real search
+ ITextSearchService2 _searchService;
+
+ // list of items to search for and tag
+ internal IList<BackgroundSearch<T>> _searchTerms = new List<BackgroundSearch<T>>();
+
+ // buffer over which search is being performed
+ ITextBuffer _buffer;
+
+ public TextSearchTagger(ITextSearchService2 searchService, ITextBuffer buffer)
+ {
+ _searchService = searchService;
+ _buffer = buffer;
+ }
+
+ #region Private Helpers
+
+ private void InvalidateTags(SnapshotSpan span)
+ {
+ EventHandler<SnapshotSpanEventArgs> tagsChangedListeners = this.TagsChanged;
+
+ if (tagsChangedListeners != null)
+ {
+ tagsChangedListeners.Invoke(this, new SnapshotSpanEventArgs(span));
+ }
+ }
+
+ private void InvalidateTags()
+ {
+ this.InvalidateTags(new SnapshotSpan(_buffer.CurrentSnapshot, 0, _buffer.CurrentSnapshot.Length));
+ }
+
+ internal void ResultsCalculated(ITextSnapshot snapshot, NormalizedSpanCollection spans)
+ {
+ if (spans.Count > 0)
+ {
+ SnapshotSpan changedSpan = new SnapshotSpan(snapshot, Span.FromBounds(spans[0].Start, spans[spans.Count - 1].End));
+ this.InvalidateTags(changedSpan);
+ }
+ }
+
+ #endregion
+
+ #region ITextSearchTagger<T> Members
+
+ private NormalizedSnapshotSpanCollection _searchSpans;
+ public NormalizedSnapshotSpanCollection SearchSpans
+ {
+ get
+ {
+ return _searchSpans;
+ }
+ set
+ {
+ if (value != null)
+ {
+ if (value.Count == 0)
+ {
+ //Treat an empty collection as if it were null.
+ value = null;
+ }
+ else if (value[0].Snapshot.TextBuffer != _buffer)
+ {
+ throw new ArgumentException("The provided SearchSpan value must belong to the same buffer as the tagger itself.");
+ }
+ }
+
+ if (value == _searchSpans)
+ {
+ return;
+ }
+
+ _searchSpans = value;
+
+ this.InvalidateTags();
+ }
+ }
+
+ public void TagTerm(string searchTerm, FindOptions searchOptions, Func<SnapshotSpan, T> tagFactory)
+ {
+ if ((searchOptions & FindOptions.SearchReverse) == FindOptions.SearchReverse)
+ {
+ throw new ArgumentException("FindOptions.SearchReverse is invalid as searches are performed forwards to ensure all matches in a requested search span are found.", "searchOptions");
+ }
+
+ if ((searchOptions & FindOptions.Wrap) == FindOptions.Wrap)
+ {
+ throw new ArgumentException("FindOptions.Wrap is invalid as searches are performed forwards with no wrapping to ensure all matches in a requested span are found.", "searchOptions");
+ }
+
+ _searchTerms.Add(new BackgroundSearch<T>(_searchService, _buffer, searchTerm, searchOptions, tagFactory, this.ResultsCalculated));
+
+ this.InvalidateTags();
+ }
+
+ public void ClearTags()
+ {
+ if (_searchTerms.Count == 0)
+ {
+ return;
+ }
+
+ ITextSnapshot snapshot = _buffer.CurrentSnapshot;
+ int start = int.MaxValue;
+ int end = int.MinValue;
+
+ foreach (BackgroundSearch<T> search in _searchTerms)
+ {
+ // Abort any ongoing background search operation since we no longer are interested in the results
+ NormalizedSnapshotSpanCollection results = search.Results;
+
+ if ((results != null) && (results.Count > 0))
+ {
+ int s = results[0].Start.TranslateTo(snapshot, PointTrackingMode.Negative);
+ if (s < start)
+ start = s;
+
+ int e = results[results.Count - 1].End.TranslateTo(snapshot, PointTrackingMode.Positive);
+ if (e > end)
+ end = e;
+ }
+
+ search.Dispose();
+ }
+
+ // Clear all currently tagging search terms
+ _searchTerms.Clear();
+
+ // Notify listeners of changed tags over the span where we had any results.
+ if (start < end)
+ this.InvalidateTags(new SnapshotSpan(snapshot, start, end - start));
+ }
+
+ #endregion
+
+ #region ITagger<T> Members
+
+ public IEnumerable<ITagSpan<T>> GetTags(NormalizedSnapshotSpanCollection requestedSpans)
+ {
+ //We should always be called with a non-empty span.
+ if (requestedSpans != null && requestedSpans.Count > 0)
+ {
+ ITextSnapshot searchSnapshot = _buffer.CurrentSnapshot;
+ requestedSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, TextSearchNavigator.TranslateTo(requestedSpans[0].Snapshot, requestedSpans, searchSnapshot));
+
+ if ((_searchSpans != null) && (_searchSpans.Count > 0))
+ {
+ //The search has been narrowed via _searchSpan ... limit the request to the search range (after making sure it is on the correct snapshot).
+ if (_searchSpans[0].Snapshot != searchSnapshot)
+ {
+ NormalizedSpanCollection newSpans = TextSearchNavigator.TranslateTo(_searchSpans[0].Snapshot, _searchSpans, searchSnapshot);
+ _searchSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, newSpans);
+ }
+
+ requestedSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, NormalizedSpanCollection.Intersection(requestedSpans, _searchSpans));
+
+ if (requestedSpans.Count == 0)
+ {
+ yield break;
+ }
+ }
+
+ foreach (var search in _searchTerms)
+ {
+ //Queue up a search if we need one.
+ search.QueueSearch(requestedSpans);
+
+ //Report any results from the search (if we've got them)
+ var results = search.Results;
+ if (results.Count > 0)
+ {
+ //Results could be on an old snapshot (and, if so, a new search has already been queued up) but we need to get the results on the current snapshot.
+ if (results[0].Snapshot != searchSnapshot)
+ {
+ results = new NormalizedSnapshotSpanCollection(searchSnapshot, TextSearchNavigator.TranslateTo(results[0].Snapshot, results, searchSnapshot));
+ }
+
+ if (_searchSpans != null)
+ {
+ results = new NormalizedSnapshotSpanCollection(searchSnapshot, NormalizedSpanCollection.Intersection(results, _searchSpans));
+ }
+
+ int start = 0;
+ foreach (var span in requestedSpans)
+ {
+ start = TextSearchTagger<T>.IndexOfContainingSpan(results, span.Start, start, false);
+ if (start >= results.Count)
+ break; //All done.
+
+ int end = TextSearchTagger<T>.IndexOfContainingSpan(results, span.End, start, true);
+
+ while (start < end)
+ {
+ T tag = search.TagFactory.Invoke(results[start]);
+
+ if (tag != null)
+ {
+ yield return new TagSpan<T>(results[start], tag);
+ }
+
+ start++;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Search spans from [start ... spans.Count-1] for a span that contains point.
+ /// If a span does contain point, return the index + 1
+ /// If a span does not contain point, return the index of the first span that starts after point (or spans.Count if there are none).
+ /// </summary>
+ private static int IndexOfContainingSpan(NormalizedSpanCollection spans, int point, int start, bool isEndPoint)
+ {
+ int lo = start;
+ int hi = spans.Count;
+ while (lo < hi)
+ {
+ int mid = (lo + hi) / 2;
+ Span s = spans[mid];
+
+ if (s.End < point)
+ {
+ lo = mid + 1;
+ }
+ else if (s.Start > point)
+ {
+ hi = mid;
+ }
+ else
+ {
+ //We know s.Start <= point <= s.End
+ //
+ //If point is an endPoint
+ // we want to return mid + 1 if a span ending at point overlaps s (== point != s.Start). Otherwise return mid.
+ //
+ //If point is a startPoint
+ // we want to return mid if a span starting at point overlaps s (== point == s.End). Otherwise return mid + 1.
+ if (isEndPoint)
+ {
+ return (point != s.Start) ? (mid + 1) : mid;
+ }
+ else
+ {
+ return (point == s.End) ? (mid + 1) : mid;
+ }
+ }
+ }
+
+ return lo;
+ }
+
+ public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
+
+ #endregion
+ }
+}
diff --git a/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs
new file mode 100644
index 0000000..fca8282
--- /dev/null
+++ b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs
@@ -0,0 +1,37 @@
+//
+// 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.Find.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+
+ using Microsoft.VisualStudio.Text.Operations;
+ using Microsoft.VisualStudio.Text.Tagging;
+
+ [Export(typeof(ITextSearchTaggerFactoryService))]
+ class TextSearchTaggerFactoryService : ITextSearchTaggerFactoryService
+ {
+ [Import]
+ private ITextSearchService2 TextSearchService = null;
+
+ #region ITextSearchTaggerFactoryService Members
+
+ public ITextSearchTagger<T> CreateTextSearchTagger<T>(ITextBuffer buffer) where T : ITag
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ // Don't return singleton instances since multiple taggers can exist per buffer
+ return new TextSearchTagger<T>(this.TextSearchService, buffer);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/ArgumentValidation.cs b/src/Text/Util/TextDataUtil/ArgumentValidation.cs
new file mode 100644
index 0000000..69ab9ef
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/ArgumentValidation.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System;
+using System.Diagnostics.Contracts;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ internal static class ArgumentValidation
+ {
+ [ContractArgumentValidator]
+ public static void NotNull(object variable, string variableName)
+ {
+ if (variable == null)
+ {
+ throw new ArgumentNullException(variableName);
+ }
+
+ Contract.EndContractBlock();
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/BufferTracker.cs b/src/Text/Util/TextDataUtil/BufferTracker.cs
new file mode 100644
index 0000000..f64e6bc
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/BufferTracker.cs
@@ -0,0 +1,151 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Projection;
+ using Microsoft.VisualStudio.Utilities;
+
+ internal sealed class BufferTracker : IDisposable
+ {
+ private static List<WeakReference> allocatedBuffers = new List<WeakReference>();
+
+ private ITextBufferFactoryService textBufferFactoryService;
+ private IProjectionBufferFactoryService projectionBufferFactoryService;
+
+ public BufferTracker(ITextBufferFactoryService textBufferFactoryService,
+ IProjectionBufferFactoryService projectionBufferFactoryService)
+ {
+ this.textBufferFactoryService = textBufferFactoryService;
+ this.projectionBufferFactoryService = projectionBufferFactoryService;
+ textBufferFactoryService.TextBufferCreated += OnBufferCreated;
+ projectionBufferFactoryService.ProjectionBufferCreated += OnBufferCreated;
+ }
+
+ public void Dispose()
+ {
+ this.textBufferFactoryService.TextBufferCreated -= OnBufferCreated;
+ this.projectionBufferFactoryService.ProjectionBufferCreated -= OnBufferCreated;
+ FreeSnapshotTrackers();
+ allocatedBuffers.Clear();
+ }
+
+ private static void FreeSnapshotTrackers()
+ {
+ for (int b = 0; b < allocatedBuffers.Count; ++b)
+ {
+ ITextBuffer buffer = allocatedBuffers[b] as ITextBuffer;
+ if (buffer != null)
+ {
+ SnapshotTracker st;
+ if (buffer.Properties.TryGetProperty<SnapshotTracker>(typeof(SnapshotTracker), out st))
+ {
+ st.Dispose();
+ buffer.Properties.RemoveProperty(typeof(SnapshotTracker));
+ }
+ }
+ }
+ }
+
+ public static void TagBuffer(ITextBuffer buffer, string tag)
+ {
+ TextUtilities.TagBuffer(buffer, tag);
+ }
+
+ public static string GetTag(ITextBuffer buffer)
+ {
+ return TextUtilities.GetTag(buffer);
+ }
+
+ public static int ReportLiveBuffers(System.IO.TextWriter writer)
+ {
+ int totalBuffers = 0;
+ if (allocatedBuffers != null)
+ {
+ for (int e = 0; e < allocatedBuffers.Count; ++e)
+ {
+ if (allocatedBuffers[e].IsAlive)
+ {
+ ++totalBuffers;
+ }
+ }
+
+ if (writer != null)
+ {
+ writer.Write
+ (String.Format
+ (System.Globalization.CultureInfo.CurrentCulture,
+ "{0}\r\n", totalBuffers));
+
+ int liveBuffers = 0;
+ for (int b = 0; b < allocatedBuffers.Count; ++b)
+ {
+ ITextBuffer buffer = allocatedBuffers[b].Target as ITextBuffer;
+ if (buffer != null)
+ {
+ ++liveBuffers;
+
+ string tag = GetTag(buffer);
+ writer.Write
+ (String.Format
+ (System.Globalization.CultureInfo.CurrentCulture,
+ "{0,5} {1}\r\n", b, !String.IsNullOrEmpty(tag) ? tag : "Untagged"));
+
+ SnapshotTracker st;
+ if (buffer.Properties.TryGetProperty<SnapshotTracker>(typeof(SnapshotTracker), out st))
+ {
+ st.ReportLiveSnapshots(writer);
+ }
+
+ foreach (KeyValuePair<Object, Object> pair in ((IPropertyOwner)buffer).Properties.PropertyList)
+ {
+ if (pair.Key.ToString() != "tag")
+ {
+ string rhsType;
+ if (pair.Value == null)
+ {
+ rhsType = "?null";
+ }
+ else
+ {
+ rhsType = pair.Value.GetType().Name;
+ if (pair.Value is WeakReference)
+ {
+ Object target = (pair.Value as WeakReference).Target;
+ if (target != null)
+ {
+ rhsType = rhsType + "(" + target.GetType().Name + ")";
+ }
+ }
+ }
+ writer.Write(String.Format(System.Globalization.CultureInfo.CurrentCulture, " {0}\r\n", rhsType));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return totalBuffers;
+ }
+
+ private void OnBufferCreated(object sender, TextBufferCreatedEventArgs e)
+ {
+ // look for a hole in allocatedBuffers. This is linear in number of extant buffers
+ // but we won't take this path except in diagnostic conditions.
+ for (int b = 0; b < allocatedBuffers.Count; ++b)
+ {
+ if (!allocatedBuffers[b].IsAlive)
+ {
+ allocatedBuffers[b].Target = e.TextBuffer;
+ return;
+ }
+ }
+ allocatedBuffers.Add(new WeakReference(e.TextBuffer));
+ e.TextBuffer.Properties.AddProperty(typeof(SnapshotTracker), new SnapshotTracker(e.TextBuffer));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Util/TextDataUtil/DifferenceCollection.cs b/src/Text/Util/TextDataUtil/DifferenceCollection.cs
new file mode 100644
index 0000000..cc2ef6b
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/DifferenceCollection.cs
@@ -0,0 +1,201 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Text.Differencing.Implementation
+{
+ /// <summary>
+ /// Represents a collection of differences over two lists of same-typed elements,
+ /// given a "maximal" match sequence (as generated from a difference algorithm).
+ /// You can enumerate over the Differences in this collection.
+ /// </summary>
+ /// <typeparam name="T">The element type of the compared lists</typeparam>
+ internal class DifferenceCollection<T> : IDifferenceCollection<T>
+ {
+ private IEnumerable<Tuple<int, int>> sequence;
+ private IList<T> originalLeft;
+ private IList<T> originalRight;
+
+ private IList<Difference> diffs;
+
+ public DifferenceCollection(IList<Difference> diffs, IList<T> originalLeft, IList<T> originalRight)
+ {
+ this.originalLeft = originalLeft;
+ this.originalRight = originalRight;
+
+ this.diffs = diffs;
+ this.sequence = new MatchEnumerator(diffs, originalLeft.Count);
+ }
+
+ public static Match CreateInitialMatch(int originalStart)
+ {
+ return (originalStart != 0) ? (new Match(new Span(0, originalStart), new Span(0, originalStart))) : (Match)null;
+ }
+
+ public static void AddDifference(int originalStart, int originalEnd, int nextOriginalEnd,
+ int modifiedStart, int modifiedEnd, int nextModifiedEnd,
+ IList<Difference> diffs, ref Match before)
+ {
+ Match after = (originalEnd != nextOriginalEnd)
+ ? (new Match(Span.FromBounds(originalEnd, nextOriginalEnd), Span.FromBounds(modifiedEnd, nextModifiedEnd)))
+ : (Match)null;
+
+ diffs.Add(new Difference(Span.FromBounds(originalStart, originalEnd), Span.FromBounds(modifiedStart, modifiedEnd),
+ before, after));
+
+ before = after;
+ }
+
+ /// <summary>
+ /// Get the original match sequence that was used to create this diff collection
+ /// </summary>
+ public IEnumerable<Tuple<int, int>> MatchSequence
+ {
+ get { return sequence; }
+ }
+
+ /// <summary>
+ /// Get the left sequence that was used to create this diff collection
+ /// </summary>
+ public IList<T> LeftSequence
+ {
+ get { return originalLeft; }
+ }
+
+ /// <summary>
+ /// Get the right sequence that was used to create this diff collection
+ /// </summary>
+ public IList<T> RightSequence
+ {
+ get { return originalRight; }
+ }
+
+ /// <summary>
+ /// Get the differences as a list.
+ /// If you just want to enumerate over the differences, you can use
+ /// the DiffCollection directly, as it is IEnumerable.
+ /// </summary>
+ public IList<Difference> Differences
+ {
+ get { return this.diffs; }
+ }
+
+ #region Private Helpers
+
+ /// <summary>
+ /// Create a list of matches from a given ordered collection of
+ /// match pairs (e.g. a MatchSequence).
+ /// </summary>
+ /// <param name="matches">An ordered collection of matching pairs, like a MatchSequence</param>
+ /// <returns>An IList of the generated Matches</returns>
+ internal static IList<Match> MatchesFromPairs(IList<Tuple<int, int>> matches)
+ {
+ if (matches.Count == 0)
+ {
+ return new List<Match>();
+ }
+
+ IList<Match> mranges = new List<Match>();
+
+ Tuple<int, int> firstMatch = matches[0];
+ int leftStart = firstMatch.Item1;
+ int leftEnd = leftStart + 1;
+
+ int rightStart = firstMatch.Item2;
+ int rightEnd = rightStart + 1;
+
+ for (int i = 1; i < matches.Count; i++)
+ {
+ Tuple<int, int> pair = matches[i];
+
+ if (pair.Item1 == leftEnd &&
+ pair.Item2 == rightEnd)
+ {
+ leftEnd++;
+ rightEnd++;
+ }
+ else
+ {
+ mranges.Add(new Match(Span.FromBounds(leftStart, leftEnd), Span.FromBounds(rightStart, rightEnd)));
+ leftStart = pair.Item1;
+ leftEnd = leftStart + 1;
+
+ rightStart = pair.Item2;
+ rightEnd = rightStart + 1;
+ }
+ }
+
+ mranges.Add(new Match(Span.FromBounds(leftStart, leftEnd), Span.FromBounds(rightStart, rightEnd)));
+
+ return mranges;
+ }
+
+ #endregion
+
+ #region IEnumerable<Difference> Members
+
+ public IEnumerator<Difference> GetEnumerator()
+ {
+ return diffs.GetEnumerator();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+
+ internal sealed class MatchEnumerator : IEnumerable<Tuple<int, int>>
+ {
+ IList<Difference> _differences;
+ int _leftCount;
+
+ public MatchEnumerator(IList<Difference> differences, int leftCount)
+ {
+ _differences = differences;
+ _leftCount = leftCount;
+ }
+
+ public IEnumerator<Tuple<int, int>> GetEnumerator()
+ {
+ int leftStart = 0;
+ int rightStart = 0;
+ if (_differences.Count != 0)
+ {
+ foreach (var difference in _differences)
+ {
+ Match m = difference.Before;
+ if (m != null)
+ {
+ for (int i = 0; i < m.Length; i++)
+ {
+ yield return new Tuple<int, int>(m.Left.Start + i, m.Right.Start + i);
+ }
+ }
+
+ leftStart = difference.Left.End;
+ rightStart = difference.Right.End;
+ }
+ }
+
+ for (int i = leftStart; (i < _leftCount); ++i)
+ {
+ yield return new Tuple<int, int>(i, i + rightStart - leftStart);
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/ExtensionSelector.cs b/src/Text/Util/TextDataUtil/ExtensionSelector.cs
new file mode 100644
index 0000000..2496711
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/ExtensionSelector.cs
@@ -0,0 +1,71 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Helper class to perform ContentType best-match against a set of extensions. This could
+ /// become a public service.
+ /// </summary>
+ internal static class ExtensionSelector
+ {
+ /// <summary>
+ /// Given a list of extensions that provide content types, filter the list and return that
+ /// subset which matches the given content type
+ /// </summary>
+ public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView>
+ (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles,
+ IContentType dataContentType)
+ where TMetadataView : IContentTypeMetadata // content type is required
+ {
+ var result = new List<Lazy<TProvider, TMetadataView>>();
+ foreach (var providerHandle in providerHandles)
+ {
+ if (ContentTypeMatch(dataContentType, providerHandle.Metadata.ContentTypes))
+ {
+ result.Add(providerHandle);
+ }
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Test whether an extension matches a content type.
+ /// </summary>
+ /// <param name="dataContentType">Content type (typically of a text buffer) against which to match an extension.</param>
+ /// <param name="extensionContentTypes">Content types from extension metadata.</param>
+ public static bool ContentTypeMatch(IContentType dataContentType, IEnumerable<string> extensionContentTypes)
+ {
+ foreach (string contentType in extensionContentTypes)
+ {
+ if (dataContentType.IsOfType(contentType))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Test whether an extension matches one of a set of content types.
+ /// </summary>
+ /// <param name="dataContentTypes">Content types (typically of text buffers in a buffer graph) against which to match an extension.</param>
+ /// <param name="extensionContentTypes">Content types from extension metadata.</param>
+ public static bool ContentTypeMatch(IEnumerable<IContentType> dataContentTypes, IEnumerable<string> extensionContentTypes)
+ {
+ foreach (IContentType bufferContentType in dataContentTypes)
+ {
+ if (ContentTypeMatch(bufferContentType, extensionContentTypes))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/FrugalList.cs b/src/Text/Util/TextDataUtil/FrugalList.cs
new file mode 100644
index 0000000..fe554fe
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/FrugalList.cs
@@ -0,0 +1,416 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+#undef STATS
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+
+#if STATS
+ static class Stats
+ {
+ public static int[] sizes = new int[20];
+ }
+#endif
+
+ /// <summary>
+ /// <para>
+ /// This implementation is intended for lists that are usually empty or have a single element.
+ /// The element type may be a struct or a class, but lists of structs are by far the most common
+ /// in the editor. We store the head of the list in a local field and allocate an array for the tail of the
+ /// list only if it the list has length greater than one. Thus singleton and empty lists require only
+ /// a single object (not counting elements of the list), as compared to the BCL version of list,
+ /// which allocates two objects for a list with a single member.
+ /// </para>
+ /// <para>
+ /// Do not use this implementation for lists that you know will have length greater than two; the
+ /// platform list implementation will be more space efficient.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The type of the list element.</typeparam>
+ public class FrugalList<T> : IList<T>, IReadOnlyList<T>
+ {
+ const int InitialTailSize = 2; // initial size of array list
+
+ static List<T> UnitaryTail = new List<T>(0); // marker that the FrugalList has length one.
+
+ T head; // first element of list
+ List<T> tail; // the balance
+
+#if STATS
+ ~FrugalList()
+ {
+ Stats.sizes[Math.Min(19, this.count)]++;
+ }
+#endif
+ #region Construction
+ // there is no constructor that takes a capacity because we are using
+ // the tail field to tell us something about the current count of elements
+ // in the list. If you are tempted to provide a capacity, and it's less than two,
+ // go ahead and use this list without a capacity. If it's greater than two, you should
+ // be using a regular list, which will be more frugal.
+
+ public FrugalList()
+ {
+ }
+
+ public FrugalList(IList<T> elements)
+ {
+ if (elements == null)
+ {
+ throw new ArgumentNullException("elements");
+ }
+ switch (elements.Count)
+ {
+ case 0:
+ break;
+ case 1:
+ this.head = elements[0];
+ this.tail = UnitaryTail;
+ break;
+ default:
+ this.head = elements[0];
+ this.tail = new List<T>(InitialTailSize);
+ for (int i = 1; i < elements.Count; ++i)
+ {
+ this.tail.Add(elements[i]);
+ }
+ break;
+ }
+ }
+ #endregion
+
+ public int Count
+ {
+ get
+ {
+ if (this.tail == null)
+ {
+ return 0;
+ }
+ else
+ {
+ return 1 + this.tail.Count;
+ }
+ }
+ }
+
+ public void AddRange(IList<T> list)
+ {
+ if (list == null)
+ {
+ throw new ArgumentNullException("list");
+ }
+
+ for (int i = 0; i < list.Count; ++i)
+ {
+ Add(list[i]);
+ }
+ }
+
+ public ReadOnlyCollection<T> AsReadOnly()
+ {
+ return new ReadOnlyCollection<T>(this);
+ }
+
+ public int RemoveAll(Predicate<T> match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+ int removed = 0;
+ for (int i = Count - 1; i >= 0; --i)
+ {
+ if (match(this[i]))
+ {
+ removed++;
+ RemoveAt(i);
+ }
+ }
+ return removed;
+ }
+
+ public void Add(T item)
+ {
+ if (Count == 0)
+ {
+ this.head = item;
+ this.tail = UnitaryTail;
+ }
+ else
+ {
+ Debug.Assert(this.tail != null);
+ if (this.tail == UnitaryTail)
+ {
+ this.tail = new List<T>(InitialTailSize);
+ }
+ this.tail.Add(item);
+ }
+ }
+
+ public int IndexOf(T item)
+ {
+ if (Count > 0)
+ {
+ if (EqualityComparer<T>.Default.Equals(this.head, item))
+ {
+ return 0;
+ }
+ else
+ {
+ int indexOf = this.tail.IndexOf(item);
+ return indexOf >= 0 ? indexOf + 1 : -1;
+ }
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ public void Insert(int index, T item)
+ {
+ if (index < 0 || index > this.Count)
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+ if (index == 0)
+ {
+ switch (Count)
+ {
+ case 0:
+ this.tail = UnitaryTail;
+ break;
+ case 1:
+ this.tail = new List<T>(InitialTailSize);
+ this.tail.Add(this.head);
+ break;
+ default:
+ this.tail.Insert(0, this.head);
+ break;
+ }
+ this.head = item;
+ }
+ else
+ {
+ Debug.Assert(Count > 0);
+ if (this.tail == UnitaryTail)
+ {
+ this.tail = new List<T>(InitialTailSize);
+ }
+ this.tail.Insert(index - 1, item);
+ }
+ }
+
+ public void RemoveAt(int index)
+ {
+ if (index < 0 || index >= Count)
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+
+ int count = Count;
+ if (index == 0)
+ {
+ if (count == 1)
+ {
+ this.head = default(T);
+ this.tail = null;
+ }
+ else
+ {
+ this.head = this.tail[0];
+ if (count == 2)
+ {
+ this.tail = UnitaryTail;
+ }
+ else
+ {
+ this.tail.RemoveAt(0);
+ }
+ }
+ }
+ else if (count == 2)
+ {
+ Debug.Assert(index == 1);
+ this.tail = UnitaryTail;
+ }
+ else
+ {
+ this.tail.RemoveAt(index - 1);
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ if (index < 0 || index >= Count)
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+
+ if (index == 0)
+ {
+ return this.head;
+ }
+ else
+ {
+ return this.tail[index - 1];
+ }
+ }
+ set
+ {
+ if (index < 0 || index >= Count)
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+
+ if (index == 0)
+ {
+ this.head = value;
+ }
+ else
+ {
+ this.tail[index - 1] = value;
+ }
+ }
+ }
+
+ public void Clear()
+ {
+ this.head = default(T);
+ this.tail = null;
+ }
+
+ public bool Contains(T item)
+ {
+ int count = Count;
+ if (count > 0)
+ {
+ if (EqualityComparer<T>.Default.Equals(this.head, item))
+ {
+ return true;
+ }
+ if (count > 1)
+ {
+ return this.tail.Contains(item);
+ }
+ }
+ return false;
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException("array");
+ }
+ int count = Count;
+ if (count > 0)
+ {
+ // let array indexing do the index checks
+ array[arrayIndex++] = this.head;
+ if (count > 1)
+ {
+ this.tail.CopyTo(array, arrayIndex);
+ }
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public bool Remove(T item)
+ {
+ if (Count > 0)
+ {
+ if (EqualityComparer<T>.Default.Equals(this.head, item))
+ {
+ RemoveAt(0);
+ return true;
+ }
+ else
+ {
+ return this.tail.Remove(item);
+ }
+ }
+ return false;
+ }
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator()
+ {
+ return new FrugalEnumerator(this);
+ }
+
+ public FrugalEnumerator GetEnumerator()
+ {
+ return new FrugalEnumerator(this);
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return new FrugalEnumerator(this);
+ }
+
+ public struct FrugalEnumerator : IEnumerator, IEnumerator<T>
+ {
+ FrugalList<T> list;
+ int position;
+
+ public FrugalEnumerator(FrugalList<T> list)
+ {
+ this.list = list;
+ this.position = -1;
+ }
+
+ public T Current
+ {
+ get
+ {
+ // if position is -1, then the behavior of Current is unspecified.
+ // for us it will throw an indexing exception.
+ return this.list[this.position];
+ }
+ }
+
+ object IEnumerator.Current
+ {
+ get { return this.list[this.position]; }
+ }
+
+ public bool MoveNext()
+ {
+ if (this.position < this.list.Count - 1)
+ {
+ this.position++;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public void Reset()
+ {
+ this.position = -1;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+
+}
diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs
new file mode 100644
index 0000000..a7412b2
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs
@@ -0,0 +1,675 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Operations that guard calls to suspicious code and log errors to registered extension error handlers.
+ /// </summary>
+ [Export]
+ [Export(typeof(IGuardedOperations))]
+ [PartCreationPolicy(CreationPolicy.Shared)]
+ internal sealed class GuardedOperations : IGuardedOperations
+ {
+ [ImportMany]
+ private List<Lazy<IExtensionErrorHandler>> _errorHandlerExports = null;
+
+ [ImportMany]
+ private List<Lazy<IExtensionPerformanceTracker>> _perTrackerExports = null;
+
+ [Import]
+ private JoinableTaskContext _joinableTaskContext;
+
+ [Import(AllowDefault = true)]
+ internal INonJoinableTaskTrackerInternal NonJoinableTaskTracker; // Optional in scenarios other than in VS process.
+
+ private FrugalList<IExtensionErrorHandler> _errorHandlers;
+ private FrugalList<IExtensionPerformanceTracker> _perfTrackers;
+
+ public GuardedOperations()
+ {
+ }
+
+ /// <summary>
+ /// For unit testing.
+ /// </summary>
+ internal GuardedOperations(JoinableTaskContext jtContext)
+ {
+ _joinableTaskContext = jtContext;
+ }
+
+ /// <summary>
+ /// For unit testing.
+ /// </summary>
+ public GuardedOperations(params IExtensionErrorHandler[] extensionErrorHandler)
+ {
+ _errorHandlers = new FrugalList<IExtensionErrorHandler>(extensionErrorHandler);
+ _perfTrackers = new FrugalList<IExtensionPerformanceTracker>();
+ }
+
+ internal static bool ReThrowIfNoHandlers { get; set; } // For unit testing.
+
+ private FrugalList<IExtensionErrorHandler> ErrorHandlers
+ {
+ get
+ {
+ if (_errorHandlers == null)
+ {
+ _errorHandlers = new FrugalList<IExtensionErrorHandler>();
+ if (_errorHandlerExports != null) // can be null during unit testing
+ {
+ foreach (var export in _errorHandlerExports)
+ {
+ try
+ {
+ var handler = export.Value;
+ if (handler != null)
+ {
+ _errorHandlers.Add(handler);
+ }
+ }
+ catch (Exception)
+ {
+ Debug.Fail("Exception instantiating error handler!");
+ }
+ }
+ }
+ }
+ return _errorHandlers;
+ }
+ }
+
+ private FrugalList<IExtensionPerformanceTracker> PerfTrackers
+ {
+ get
+ {
+ if (_perfTrackers == null)
+ {
+ _perfTrackers = new FrugalList<IExtensionPerformanceTracker>();
+ if (_perTrackerExports != null) // can be null during unit testing
+ {
+ foreach (var export in _perTrackerExports)
+ {
+ try
+ {
+ var perfTracker = export.Value;
+ if (perfTracker != null)
+ {
+ _perfTrackers.Add(perfTracker);
+ }
+ }
+ catch (Exception)
+ {
+ Debug.Fail("Exception instantiating perf tracker");
+ }
+ }
+ }
+ }
+ return _perfTrackers;
+ }
+ }
+
+ public TExtensionInstance InvokeBestMatchingFactory<TExtensionFactory, TExtensionInstance, TMetadataView>
+ (IList<Lazy<TExtensionFactory, TMetadataView>> providerHandles,
+ IContentType dataContentType,
+ Func<TExtensionFactory, TExtensionInstance> getter,
+ IContentTypeRegistryService contentTypeRegistryService,
+ object errorSource)
+ where TMetadataView : IContentTypeMetadata
+ where TExtensionFactory : class
+ {
+ var factory = InvokeBestMatchingFactory(providerHandles, dataContentType, contentTypeRegistryService, errorSource);
+
+ if (factory == null)
+ {
+ return default(TExtensionInstance);
+ }
+
+ TExtensionInstance extensionInstance = default(TExtensionInstance);
+ this.CallExtensionPoint(errorSource, () => extensionInstance = getter(factory));
+ return extensionInstance;
+ }
+
+ public TExtension InvokeBestMatchingFactory<TExtension, TMetadataView>
+ (IList<Lazy<TExtension, TMetadataView>> providerHandles,
+ IContentType dataContentType,
+ IContentTypeRegistryService contentTypeRegistryService,
+ object errorSource)
+ where TMetadataView : IContentTypeMetadata
+ {
+ var candidates = new List<Lazy<TExtension, TMetadataView>>();
+ foreach (var providerHandle in providerHandles)
+ {
+ foreach (string contentTypeName in providerHandle.Metadata.ContentTypes)
+ {
+ if (string.Compare(dataContentType.TypeName, contentTypeName, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ // we have an exact match--no need to look further if this one is happy
+ TExtension factory = InstantiateExtension(errorSource, providerHandle);
+ if (factory != null)
+ {
+ return factory;
+ }
+ }
+ else if (dataContentType.IsOfType(contentTypeName))
+ {
+ candidates.Add(providerHandle);
+ break;
+ }
+ }
+ }
+
+ SortCandidates(candidates, dataContentType, contentTypeRegistryService);
+
+ for (int c = 0; c < candidates.Count; ++c)
+ {
+ TExtension factory = InstantiateExtension(errorSource, candidates[c]);
+ if (factory != null)
+ {
+ return factory;
+ }
+ }
+
+ // no suitable provider found
+ return default(TExtension);
+ }
+
+ public List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView>
+ (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories,
+ Func<TExtensionFactory, TExtensionInstance> getter,
+ IContentType dataContentType,
+ object errorSource)
+ where TMetadataView : IContentTypeMetadata // content type is required
+ where TExtensionFactory : class
+ where TExtensionInstance : class
+ {
+ var result = new List<TExtensionInstance>();
+ foreach (var lazyFactory in lazyFactories)
+ {
+ if (ExtensionSelector.ContentTypeMatch(dataContentType, lazyFactory.Metadata.ContentTypes))
+ {
+ try
+ {
+ TExtensionFactory factory = lazyFactory.Value;
+ if (factory != null)
+ {
+ TExtensionInstance instance = getter(factory);
+ if (instance != null)
+ {
+ result.Add(instance);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ }
+ }
+ }
+ return result;
+ }
+
+ // The algorithm here is that assets can have a Name attribute and one or more Replaces attribute.
+ // Assets without names are treated normally (they are always considered eligible).
+ // Named assets are considered ineligible if:
+ // There is a "better" asset with the same name (better means a more specific content type).
+ // There is another assert with a Replaces attribute that matches the name of the asset.
+ public IEnumerable<Lazy<TExtensionFactory, TMetadataView>> FindEligibleFactories<TExtensionFactory, TMetadataView>
+ (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories,
+ IContentType dataContentType,
+ IContentTypeRegistryService contentTypeRegistryService)
+ where TMetadataView : INamedContentTypeMetadata // content type is required
+ where TExtensionFactory : class
+ {
+ Dictionary<string, List<Lazy<TExtensionFactory, TMetadataView>>> namedFactories = null;
+ HashSet<string> replaced = null;
+ foreach (var lazyFactory in lazyFactories)
+ {
+ if (ExtensionSelector.ContentTypeMatch(dataContentType, lazyFactory.Metadata.ContentTypes))
+ {
+ if (string.IsNullOrEmpty(lazyFactory.Metadata.Name))
+ {
+ yield return lazyFactory;
+ }
+ else
+ {
+ if (namedFactories == null)
+ {
+ namedFactories = new Dictionary<string, List<Lazy<TExtensionFactory, TMetadataView>>>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ List<Lazy<TExtensionFactory, TMetadataView>> factories;
+ if (!namedFactories.TryGetValue(lazyFactory.Metadata.Name, out factories))
+ {
+ factories = new List<Lazy<TExtensionFactory, TMetadataView>>();
+ namedFactories.Add(lazyFactory.Metadata.Name, factories);
+ }
+
+ factories.Add(lazyFactory);
+
+ if (lazyFactory.Metadata.Replaces != null)
+ {
+ if (replaced == null)
+ {
+ replaced = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ foreach (var s in lazyFactory.Metadata.Replaces)
+ {
+ replaced.Add(s);
+ }
+ }
+ }
+ }
+ }
+
+ if (namedFactories != null)
+ {
+ foreach (var candidates in namedFactories.Values)
+ {
+ var candidate = candidates[0];
+ if ((replaced == null) || !replaced.Contains(candidate.Metadata.Name))
+ {
+ SortCandidates(candidates, dataContentType, contentTypeRegistryService);
+ yield return candidates[0];
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Given a list of factory extensions that provide content types, filter the list, instantiate that
+ /// subset which matches the given content type, and invoke the factory method. Return the non-null results.
+ /// </summary>
+ public List<TExtensionInstance> InvokeEligibleFactories<TExtensionInstance, TExtensionFactory, TMetadataView>
+ (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories,
+ Func<TExtensionFactory, TExtensionInstance> getter,
+ IContentType dataContentType,
+ IContentTypeRegistryService contentTypeRegistryService,
+ object errorSource)
+ where TMetadataView : INamedContentTypeMetadata // content type is required
+ where TExtensionFactory : class
+ where TExtensionInstance : class
+ {
+ var result = new List<TExtensionInstance>();
+ foreach (var lazyFactory in FindEligibleFactories(lazyFactories, dataContentType, contentTypeRegistryService))
+ {
+ try
+ {
+ TExtensionFactory factory = lazyFactory.Value;
+ if (factory != null)
+ {
+ TExtensionInstance instance = getter(factory);
+ if (instance != null)
+ {
+ result.Add(instance);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ }
+ }
+
+ return result;
+ }
+
+ public TExtension InstantiateExtension<TExtension>(object errorSource, Lazy<TExtension> provider)
+ {
+ try
+ {
+ return provider.Value;
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ return default(TExtension);
+ }
+ }
+
+ public TExtension InstantiateExtension<TExtension, TMetadata>(object errorSource, Lazy<TExtension, TMetadata> provider)
+ {
+ try
+ {
+ return provider.Value;
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ return default(TExtension);
+ }
+ }
+
+ public TExtensionInstance InstantiateExtension<TExtension, TMetadata, TExtensionInstance>(
+ object errorSource, Lazy<TExtension, TMetadata> provider, Func<TExtension, TExtensionInstance> getter)
+ {
+ try
+ {
+ return getter(provider.Value);
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ return default(TExtensionInstance);
+ }
+ }
+
+ public void CallExtensionPoint(object errorSource, Action call)
+ {
+ try
+ {
+ BeforeCallingEventHandler(call);
+ call();
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ }
+ finally
+ {
+ AfterCallingEventHandler(call);
+ }
+ }
+
+ public T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow)
+ {
+ try
+ {
+ BeforeCallingEventHandler(call);
+ return call();
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+
+ return valueOnThrow;
+ }
+ finally
+ {
+ AfterCallingEventHandler(call);
+ }
+ }
+
+ public void CallExtensionPoint(Action call)
+ {
+ this.CallExtensionPoint(errorSource: null, call: call);
+ }
+
+ public T CallExtensionPoint<T>(Func<T> call, T valueOnThrow)
+ {
+ return this.CallExtensionPoint(errorSource: null, call: call, valueOnThrow: valueOnThrow);
+ }
+
+ public async Task CallExtensionPointAsync(object errorSource, Func<Task> asyncAction)
+ {
+ try
+ {
+ await asyncAction();
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ }
+ }
+
+ public async Task CallExtensionPointAsync(Func<Task> asyncAction)
+ {
+ await CallExtensionPointAsync(errorSource: null, asyncAction: asyncAction);
+ }
+
+ public async Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow)
+ {
+ try
+ {
+ return await asyncCall();
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e);
+ return valueOnThrow;
+ }
+ }
+
+ public async Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow)
+ {
+ return await CallExtensionPointAsync<T>(errorSource: null, asyncCall: asyncCall, valueOnThrow: valueOnThrow);
+ }
+
+ public void RaiseEvent(object sender, EventHandler eventHandlers)
+ {
+ if (eventHandlers == null)
+ {
+ return;
+ }
+
+ var handlers = eventHandlers.GetInvocationList();
+
+ foreach (EventHandler handler in handlers)
+ {
+ try
+ {
+ BeforeCallingEventHandler(handler);
+ handler(sender, EventArgs.Empty);
+ }
+ catch (Exception e)
+ {
+ HandleException(sender, e);
+ }
+ finally
+ {
+ AfterCallingEventHandler(handler);
+ }
+ }
+ }
+
+ public void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs
+ {
+ if (eventHandlers == null)
+ {
+ return;
+ }
+ var handlers = eventHandlers.GetInvocationList();
+
+ foreach (EventHandler<TArgs> handler in handlers)
+ {
+ try
+ {
+ BeforeCallingEventHandler(handler);
+ handler(sender, args);
+ }
+ catch (Exception e)
+ {
+ HandleException(sender, e);
+ }
+ finally
+ {
+ AfterCallingEventHandler(handler);
+ }
+ }
+ }
+
+ private void AfterCallingEventHandler(Delegate handler)
+ {
+ if (PerfTrackers.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var perfTracker in PerfTrackers)
+ {
+ try
+ {
+ perfTracker.AfterCallingEventHandler(handler);
+ }
+ catch (Exception e)
+ {
+ HandleException(perfTracker, e);
+ }
+ }
+ }
+
+ private void BeforeCallingEventHandler(Delegate handler)
+ {
+ if (PerfTrackers.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var perfTracker in PerfTrackers)
+ {
+ try
+ {
+ perfTracker.BeforeCallingEventHandler(handler);
+ }
+ catch (Exception e)
+ {
+ HandleException(perfTracker, e);
+ }
+ }
+ }
+
+ public void HandleException(object errorSource, Exception e)
+ {
+ bool handled = false;
+ foreach (var errorHandler in ErrorHandlers)
+ {
+ try
+ {
+ errorHandler.HandleError(errorSource, e);
+ handled = true;
+ }
+ catch (Exception doubleFaultException)
+ {
+ // TODO: What is the right behavior here?
+ Debug.Fail(doubleFaultException.ToString());
+ }
+ }
+ if (!handled)
+ {
+ // TODO: What is the right behavior here?
+ Debug.Fail(e.ToString());
+
+ if (GuardedOperations.ReThrowIfNoHandlers)
+ throw new Exception("Unhandled exception.", e);
+ }
+ }
+
+ private static void SortCandidates<TExtension, TMetadataView>(List<Lazy<TExtension, TMetadataView>> candidates, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService)
+ where TMetadataView : IContentTypeMetadata
+ {
+ if (candidates.Count > 1)
+ {
+ var contentTypes = new List<IContentType>();
+ foreach (var c in candidates)
+ {
+ foreach (string contentTypeName in c.Metadata.ContentTypes)
+ {
+ if (dataContentType.IsOfType(contentTypeName))
+ {
+ var type = contentTypeRegistryService.GetContentType(contentTypeName);
+ if (!contentTypes.Contains(type))
+ {
+ contentTypes.Add(type);
+ }
+ }
+ }
+ }
+
+ contentTypes.Sort(CompareContentTypes);
+ candidates.Sort((left, right) =>
+ {
+ int leftIndex = BestContentTypeScore(left.Metadata.ContentTypes, contentTypes);
+ int rightIndex = BestContentTypeScore(right.Metadata.ContentTypes, contentTypes);
+
+ return leftIndex - rightIndex; // Sort these in ascending order.
+ });
+ }
+ }
+
+ private static int BestContentTypeScore(IEnumerable<string> contentTypes, List<IContentType> sortedContentTypes)
+ {
+ return contentTypes.Min(s => ContentTypeScore(s, sortedContentTypes));
+ }
+
+ private static int ContentTypeScore(string contentTypeName, List<IContentType> sortedContentTypes)
+ {
+ for (int i = 0; (i < sortedContentTypes.Count); ++i)
+ {
+ if (string.Compare(sortedContentTypes[i].TypeName, contentTypeName, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return i;
+ }
+ }
+
+ return sortedContentTypes.Count;
+ }
+
+ private static int CompareContentTypes(IContentType left, IContentType right)
+ {
+ if (left == right)
+ {
+ return 0;
+ }
+ else
+ {
+ if (left.IsOfType(right.TypeName))
+ {
+ return -1;
+ }
+ else if (right.IsOfType(left.TypeName))
+ {
+ return +1;
+ }
+ else
+ {
+ // the content types are unrelated, use alpha order of their names
+ return string.Compare(left.TypeName, right.TypeName, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+
+ public Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs
+ {
+ return _joinableTaskContext.Factory.RunAsync(async () =>
+ {
+ await TaskScheduler.Default;
+
+ if (eventHandlers == null)
+ {
+ return;
+ }
+
+ var handlers = eventHandlers.GetInvocationList();
+
+ foreach (AsyncEventHandler<TArgs> handler in handlers)
+ {
+ try
+ {
+ BeforeCallingEventHandler(handler);
+ await handler(sender, args);
+ }
+ catch (Exception e)
+ {
+ HandleException(sender, e);
+ }
+ finally
+ {
+ AfterCallingEventHandler(handler);
+ }
+ }
+ }).Task;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs b/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs
new file mode 100644
index 0000000..38dfe0c
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs
@@ -0,0 +1,15 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// MEF metadata definition for <see cref="IEncodingDetector"/>.
+ /// </summary>
+ public interface IEncodingDetectorMetadata : IOrderable, IContentTypeMetadata
+ {
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs b/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs
new file mode 100644
index 0000000..76f7e97
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs
@@ -0,0 +1,15 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Metadata which includes Ordering and Content Types
+ /// </summary>
+ public interface IOrderableContentTypeMetadata : IContentTypeMetadata, IOrderable
+ {
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/ListUtilities.cs b/src/Text/Util/TextDataUtil/ListUtilities.cs
new file mode 100644
index 0000000..a61d5cb
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/ListUtilities.cs
@@ -0,0 +1,64 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ /// <summary>
+ /// Handy list-oriented utilities.
+ /// </summary>
+ internal static class ListUtilities
+ {
+ /// <summary>
+ /// Do a binary search in <paramref name="list"/> for an element that matches <paramref name="target"/>
+ /// </summary>
+ /// <param name="list">List to search.</param>
+ /// <param name="target">Object of the search.</param>
+ /// <param name="compare">Comparison function between an element and target (returns &lt; 0 if e comes before t, 0 if e matches, &gt; 0 if e comes after).
+ /// <param name="index">Index of the matching element (or, if there is no exact match, index of the element that follows it).</param>
+ /// <returns>true if an exact match was found.</returns>
+ /// <remarks>Yes, I know there is List.BinarySearch but that doesn't do exactly what I need most of the time.</remarks>
+ public static bool BinarySearch<E>(IList<E> list, Func<E, int> compare, out int index)
+ {
+ int lo = 0;
+ int hi = list.Count;
+
+ while (lo < hi)
+ {
+ index = (lo + hi) / 2;
+
+ int cmp = compare(list[index]);
+ if (cmp < 0)
+ {
+ lo = index + 1;
+ }
+ else if (cmp == 0)
+ {
+ return true;
+ }
+ else
+ {
+ hi = index;
+ }
+ }
+
+ index = lo;
+ return false;
+ }
+
+ public static T? FirstOrNullable<T>(this IEnumerable<T> source)
+ where T : struct
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ return source.Cast<T?>().FirstOrDefault();
+ }
+ }
+ }
diff --git a/src/Text/Util/TextDataUtil/MappingHelper.cs b/src/Text/Util/TextDataUtil/MappingHelper.cs
new file mode 100644
index 0000000..add43f2
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/MappingHelper.cs
@@ -0,0 +1,275 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Projection;
+
+ internal static class MappingHelper
+ {
+ //These two methods are nearly duplicates of one another but delegates can be expensive and this is inner loop code.
+ internal static ITextSnapshot FindCorrespondingSnapshot(ITextSnapshot sourceSnapshot, ITextBuffer targetBuffer)
+ {
+ if (sourceSnapshot.TextBuffer == targetBuffer)
+ {
+ // simple case: single buffer
+ return sourceSnapshot;
+ }
+ else
+ {
+ IProjectionSnapshot2 projSnap = sourceSnapshot as IProjectionSnapshot2;
+ if (projSnap != null)
+ {
+ return projSnap.GetMatchingSnapshotInClosure(targetBuffer);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ internal static ITextSnapshot FindCorrespondingSnapshot(ITextSnapshot sourceSnapshot, Predicate<ITextBuffer> match)
+ {
+ if (match(sourceSnapshot.TextBuffer))
+ {
+ // simple case: single buffer
+ return sourceSnapshot;
+ }
+ else
+ {
+ IProjectionSnapshot2 projSnap = sourceSnapshot as IProjectionSnapshot2;
+ if (projSnap != null)
+ {
+ return projSnap.GetMatchingSnapshotInClosure(match);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ internal static NormalizedSnapshotSpanCollection MapDownToBufferNoTrack(SnapshotSpan sourceSpan, ITextBuffer targetBuffer, bool mapByContentType = false)
+ {
+ FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>();
+
+ MapDownToFirstMatchNoTrack(sourceSpan, (ITextBuffer b) => { return b == targetBuffer; }, mappedSpans, mapByContentType);
+
+ return new NormalizedSnapshotSpanCollection(mappedSpans);
+ }
+
+ internal static void MapDownToBufferNoTrack(SnapshotSpan sourceSpan, ITextBuffer targetBuffer, IList<SnapshotSpan> mappedSpans, bool mapByContentType = false)
+ {
+ // Most of the time, the sourceSpan will map to the targetBuffer as a single span, rather than being split.
+ // Since this method is called a lot, we'll assume first that we'll get a single span and don't need to
+ // allocate a stack to keep track of unmapped spans. If that fails we'll fall back on the more expensive approach.
+ // Scroll around for a while and this saves a bunch of allocations.
+ SnapshotSpan mappedSpan = sourceSpan;
+ while (true)
+ {
+ if (mappedSpan.Snapshot.TextBuffer == targetBuffer)
+ {
+ mappedSpans.Add(mappedSpan);
+ return;
+ }
+ else
+ {
+ IProjectionSnapshot mappedSpanProjectionSnapshot = mappedSpan.Snapshot as IProjectionSnapshot;
+ if (mappedSpanProjectionSnapshot != null &&
+ (!mapByContentType || mappedSpanProjectionSnapshot.ContentType.IsOfType("projection")))
+ {
+ var mappedDownSpans = mappedSpanProjectionSnapshot.MapToSourceSnapshots(mappedSpan);
+ if (mappedDownSpans.Count == 1)
+ {
+ mappedSpan = mappedDownSpans[0];
+ continue;
+ }
+ else if (mappedDownSpans.Count == 0)
+ {
+ return;
+ }
+ else
+ {
+ // the projection mapping resulted in more than one span
+ FrugalList<SnapshotSpan> unmappedSpans = new FrugalList<SnapshotSpan>(mappedDownSpans);
+ SplitMapDownToBufferNoTrack(unmappedSpans, targetBuffer, mappedSpans, mapByContentType);
+ return;
+ }
+ }
+ else
+ {
+ // either it's a projection buffer we can't look through, or it's
+ // an ordinary buffer that didn't match
+ return;
+ }
+ }
+ }
+ }
+
+ private static void SplitMapDownToBufferNoTrack(FrugalList<SnapshotSpan> unmappedSpans, ITextBuffer targetBuffer, IList<SnapshotSpan> mappedSpans, bool mapByContentType)
+ {
+ while (unmappedSpans.Count > 0)
+ {
+ SnapshotSpan span = unmappedSpans[unmappedSpans.Count - 1];
+ unmappedSpans.RemoveAt(unmappedSpans.Count - 1);
+
+ if (span.Snapshot.TextBuffer == targetBuffer)
+ {
+ mappedSpans.Add(span);
+ }
+ else
+ {
+ IProjectionSnapshot spanSnapshotAsProjection = span.Snapshot as IProjectionSnapshot;
+ if (spanSnapshotAsProjection != null &&
+ (!mapByContentType || span.Snapshot.TextBuffer.ContentType.IsOfType("projection")))
+ {
+ unmappedSpans.AddRange(spanSnapshotAsProjection.MapToSourceSnapshots(span));
+ }
+ }
+ }
+ }
+
+ internal static void MapDownToFirstMatchNoTrack(SnapshotSpan sourceSpan, Predicate<ITextBuffer> match, IList<SnapshotSpan> mappedSpans, bool mapByContentType = false)
+ {
+ // Most of the time, the sourceSpan will map to the targetBuffer as a single span, rather than being split.
+ // Since this method is called a lot, we'll assume first that we'll get a single span and don't need to
+ // allocate a stack to keep track of unmapped spans. If that fails we'll fall back on the more expensive approach.
+ // Scroll around for a while and this saves a bunch of allocations.
+ SnapshotSpan mappedSpan = sourceSpan;
+ while (true)
+ {
+ if (match(mappedSpan.Snapshot.TextBuffer))
+ {
+ mappedSpans.Add(mappedSpan);
+ return;
+ }
+ else
+ {
+ IProjectionSnapshot mappedSpanProjectionSnapshot = mappedSpan.Snapshot as IProjectionSnapshot;
+ if (mappedSpanProjectionSnapshot != null &&
+ (!mapByContentType || mappedSpanProjectionSnapshot.ContentType.IsOfType("projection")))
+ {
+ var mappedDownSpans = mappedSpanProjectionSnapshot.MapToSourceSnapshots(mappedSpan);
+ if (mappedDownSpans.Count == 1)
+ {
+ mappedSpan = mappedDownSpans[0];
+ continue;
+ }
+ else if (mappedDownSpans.Count == 0)
+ {
+ return;
+ }
+ else
+ {
+ // the projection mapping resulted in more than one span
+ FrugalList<SnapshotSpan> unmappedSpans = new FrugalList<SnapshotSpan>(mappedDownSpans);
+ SplitMapDownToFirstMatchNoTrack(unmappedSpans, match, mappedSpans, mapByContentType);
+ return;
+ }
+ }
+ else
+ {
+ // either it's a projection buffer we can't look through, or it's
+ // an ordinary buffer that didn't match
+ return;
+ }
+ }
+ }
+ }
+
+ private static void SplitMapDownToFirstMatchNoTrack(FrugalList<SnapshotSpan> unmappedSpans, Predicate<ITextBuffer> match, IList<SnapshotSpan> mappedSpans, bool mapByContentType)
+ {
+ ITextSnapshot matchingSnapshot = null;
+
+ while (unmappedSpans.Count > 0)
+ {
+ SnapshotSpan span = unmappedSpans[unmappedSpans.Count - 1];
+ unmappedSpans.RemoveAt(unmappedSpans.Count - 1);
+
+ if (span.Snapshot == matchingSnapshot)
+ {
+ mappedSpans.Add(span);
+ }
+ else if (match(span.Snapshot.TextBuffer))
+ {
+ mappedSpans.Add(span);
+ matchingSnapshot = span.Snapshot;
+ }
+ else
+ {
+ IProjectionSnapshot spanSnapshotAsProjection = span.Snapshot as IProjectionSnapshot;
+ if (spanSnapshotAsProjection != null &&
+ (!mapByContentType || span.Snapshot.TextBuffer.ContentType.IsOfType("projection")))
+ {
+ unmappedSpans.AddRange(spanSnapshotAsProjection.MapToSourceSnapshots(span));
+ }
+ }
+ }
+ }
+
+ internal static SnapshotPoint? MapDownToBufferNoTrack(SnapshotPoint position, ITextBuffer targetBuffer, PositionAffinity affinity)
+ {
+ while (position.Snapshot.TextBuffer != targetBuffer)
+ {
+ IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot;
+ if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0))
+ {
+ return null;
+ }
+
+ position = projSnap.MapToSourceSnapshot(position, affinity);
+ }
+ return position;
+ }
+
+ internal static SnapshotPoint? MapDownToFirstMatchNoTrack(SnapshotPoint position, Predicate<ITextBuffer> match, PositionAffinity affinity)
+ {
+ while (!match(position.Snapshot.TextBuffer))
+ {
+ IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot;
+ if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0))
+ {
+ return null;
+ }
+
+ position = projSnap.MapToSourceSnapshot(position, affinity);
+ }
+ return position;
+ }
+
+ internal static SnapshotPoint? MapDownToBufferNoTrack(SnapshotPoint position, ITextBuffer targetBuffer)
+ {
+ while (position.Snapshot.TextBuffer != targetBuffer)
+ {
+ IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot;
+ if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0))
+ {
+ return null;
+ }
+
+ position = projSnap.MapToSourceSnapshot(position);
+ }
+
+ return position;
+ }
+
+ internal static SnapshotPoint? MapDownToFirstMatchNoTrack(SnapshotPoint position, Predicate<ITextBuffer> match)
+ {
+ while (!match(position.Snapshot.TextBuffer))
+ {
+ IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot;
+ if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0))
+ {
+ return null;
+ }
+
+ position = projSnap.MapToSourceSnapshot(position);
+ }
+ return position;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs
new file mode 100644
index 0000000..3e6c110
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs
@@ -0,0 +1,169 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using Microsoft.VisualStudio.Text.Projection;
+
+ internal class MappingPointSnapshot : IMappingPoint
+ {
+ internal ITextSnapshot _root;
+ internal SnapshotPoint _anchor;
+ internal PointTrackingMode _trackingMode;
+ IBufferGraph _graph;
+ internal bool _unmappable = false;
+
+ public static IMappingPoint Create(ITextSnapshot root, SnapshotPoint anchor, PointTrackingMode trackingMode, IBufferGraph graph)
+ {
+ return new MappingPointSnapshot(root, anchor, trackingMode, graph);
+ }
+
+ private MappingPointSnapshot(ITextSnapshot root, SnapshotPoint anchor, PointTrackingMode trackingMode, IBufferGraph graph)
+ {
+ //Anchor and root are expected to be from concurrent snapshots
+ ITextSnapshot correspondingAnchorSnapshot = MappingHelper.FindCorrespondingSnapshot(root, anchor.Snapshot.TextBuffer);
+
+ _root = root;
+ if (correspondingAnchorSnapshot != null)
+ _anchor = anchor.TranslateTo(correspondingAnchorSnapshot, trackingMode);
+ else
+ {
+ _anchor = anchor;
+ _unmappable = true;
+ }
+ _trackingMode = trackingMode;
+ _graph = graph;
+ }
+
+ public SnapshotPoint? GetPoint(ITextBuffer targetBuffer, PositionAffinity affinity)
+ {
+ if (targetBuffer == null)
+ throw new ArgumentNullException("targetBuffer");
+ if (_unmappable)
+ return null;
+
+ //Try mapping down first.
+ SnapshotPoint? mappedPoint = MappingHelper.MapDownToBufferNoTrack(_anchor, targetBuffer, affinity);
+
+ if (!mappedPoint.HasValue)
+ {
+ //Unable to map down ... try mapping up.
+ return this.MapUpToBufferNoTrack(targetBuffer, affinity);
+ }
+
+ return mappedPoint;
+ }
+
+ public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity)
+ {
+ if (targetSnapshot == null)
+ throw new ArgumentNullException("targetSnapshot");
+ if (_unmappable)
+ return null;
+
+ SnapshotPoint? mappedPoint = this.GetPoint(targetSnapshot.TextBuffer, affinity);
+ if (mappedPoint.HasValue && (mappedPoint.Value.Snapshot != targetSnapshot))
+ {
+ mappedPoint = mappedPoint.Value.TranslateTo(targetSnapshot, _trackingMode);
+ }
+
+ return mappedPoint;
+ }
+
+ public SnapshotPoint? GetPoint(Predicate<ITextBuffer> match, PositionAffinity affinity)
+ {
+ if (match == null)
+ throw new ArgumentNullException("match");
+ if (_unmappable)
+ return null;
+
+ //Try mapping down first.
+ SnapshotPoint? mappedPoint = MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match, affinity);
+
+ if (!mappedPoint.HasValue)
+ {
+ //Unable to map down ... try mapping up.
+ return this.MapUpToFirstMatchNoTrack(match, affinity);
+ }
+
+ return mappedPoint;
+ }
+
+ public SnapshotPoint? GetInsertionPoint(Predicate<ITextBuffer> match)
+ {
+ if (match == null)
+ throw new ArgumentNullException("match");
+ if (_unmappable)
+ return null;
+
+ return MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match);
+ }
+
+ public ITextBuffer AnchorBuffer
+ {
+ get { return _anchor.Snapshot.TextBuffer; }
+ }
+
+ public IBufferGraph BufferGraph
+ {
+ get { return _graph; }
+ }
+
+ private SnapshotPoint? MapUpToBufferNoTrack(ITextBuffer targetBuffer, PositionAffinity affinity)
+ {
+ ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer);
+ if (targetSnapshot != null)
+ {
+ //Map _anchor up to targetSnapshot (they should be concurrent snapshots)
+ return MapUpToSnapshotNoTrack(targetSnapshot, affinity);
+ }
+
+ return null;
+ }
+
+ private SnapshotPoint? MapUpToFirstMatchNoTrack(Predicate<ITextBuffer> match, PositionAffinity affinity)
+ {
+ ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, match);
+ if (targetSnapshot != null)
+ {
+ //Map _anchor up to targetSnapshot (they should be concurrent snapshots)
+ return MapUpToSnapshotNoTrack(targetSnapshot, affinity);
+ }
+
+ return null;
+ }
+
+ private SnapshotPoint? MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, PositionAffinity affinity)
+ {
+ return MapUpToSnapshotNoTrack(targetSnapshot, _anchor, affinity);
+ }
+
+ public static SnapshotPoint? MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, SnapshotPoint anchor, PositionAffinity affinity)
+ {
+ if (anchor.Snapshot == targetSnapshot)
+ return anchor;
+ else
+ {
+ IProjectionSnapshot targetAsProjection = targetSnapshot as IProjectionSnapshot;
+ if (targetAsProjection != null)
+ {
+ var sourceSnapshots = targetAsProjection.SourceSnapshots;
+ for (int s = 0; s < sourceSnapshots.Count; ++s)
+ {
+ SnapshotPoint? downPoint = MapUpToSnapshotNoTrack(sourceSnapshots[s], anchor, affinity);
+ if (downPoint.HasValue)
+ {
+ SnapshotPoint? result = targetAsProjection.MapFromSourceSnapshot(downPoint.Value, affinity);
+ if (result.HasValue)
+ return result;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs
new file mode 100644
index 0000000..88714fd
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs
@@ -0,0 +1,209 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Projection;
+
+ internal class MappingSpanSnapshot : IMappingSpan
+ {
+ private ITextSnapshot _root;
+ private SnapshotSpan _anchor;
+ private SpanTrackingMode _trackingMode;
+ private IBufferGraph _graph;
+ private bool _unmappable = false;
+
+ public static IMappingSpan Create(ITextSnapshot root, SnapshotSpan anchor, SpanTrackingMode trackingMode, IBufferGraph graph)
+ {
+ return new MappingSpanSnapshot(root, anchor, trackingMode, graph);
+ }
+
+ private MappingSpanSnapshot(ITextSnapshot root, SnapshotSpan anchor, SpanTrackingMode trackingMode, IBufferGraph graph)
+ {
+ //Anchor and root are expected to be from concurrent snapshots
+ ITextSnapshot correspondingAnchorSnapshot = MappingHelper.FindCorrespondingSnapshot(root, anchor.Snapshot.TextBuffer);
+
+ _root = root;
+ if (correspondingAnchorSnapshot != null)
+ _anchor = anchor.TranslateTo(correspondingAnchorSnapshot, trackingMode);
+ else
+ {
+ _anchor = anchor;
+ _unmappable = true;
+ }
+ _trackingMode = trackingMode;
+ _graph = graph;
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(ITextBuffer targetBuffer)
+ {
+ if (targetBuffer == null)
+ throw new ArgumentNullException("targetBuffer");
+
+ if (_unmappable)
+ return NormalizedSnapshotSpanCollection.Empty;
+
+ if (targetBuffer.Properties.ContainsProperty("IdentityMapping"))
+ {
+ // text buffer properties uses the hybrid dictionary, which requires TWO lookups to determine that
+ // a key is not present. Since this test usually fails, do it the hard way (the second lookup shows up
+ // in scrolling profiles).
+ ITextBuffer doppelganger = (ITextBuffer)targetBuffer.Properties["IdentityMapping"];
+ if (doppelganger == _anchor.Snapshot.TextBuffer)
+ {
+ // We are mapping up from a doppelganger buffer; the coordinates will be the same. We
+ // just need to figure out the right snapshot.
+ ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer);
+ return new NormalizedSnapshotSpanCollection(new SnapshotSpan(targetSnapshot, _anchor.Span));
+ }
+ }
+
+ //Try mapping down first.
+ FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>();
+ MappingHelper.MapDownToBufferNoTrack(_anchor, targetBuffer, mappedSpans);
+ if (mappedSpans.Count == 0)
+ {
+ //Unable to map down ... try mapping up.
+ this.MapUpToBufferNoTrack(targetBuffer, mappedSpans);
+ }
+
+ return new NormalizedSnapshotSpanCollection(mappedSpans);
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot)
+ {
+ if (targetSnapshot == null)
+ throw new ArgumentNullException("targetSnapshot");
+ if (_unmappable)
+ return NormalizedSnapshotSpanCollection.Empty;
+
+ NormalizedSnapshotSpanCollection results = this.GetSpans(targetSnapshot.TextBuffer);
+
+ if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot))
+ {
+ FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>();
+ foreach (SnapshotSpan s in results)
+ {
+ translatedSpans.Add(s.TranslateTo(targetSnapshot, _trackingMode));
+ }
+
+ results = new NormalizedSnapshotSpanCollection(translatedSpans);
+ }
+
+ return results;
+ }
+
+ public NormalizedSnapshotSpanCollection GetSpans(Predicate<ITextBuffer> match)
+ {
+ if (_unmappable)
+ return NormalizedSnapshotSpanCollection.Empty;
+
+ //Try mapping down first.
+ FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>();
+ MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match, mappedSpans);
+ if (mappedSpans.Count == 0)
+ {
+ //Unable to map down ... try mapping up.
+ this.MapUpToBufferNoTrack(match, mappedSpans);
+ }
+
+ return new NormalizedSnapshotSpanCollection(mappedSpans);
+ }
+
+ public IMappingPoint Start
+ {
+ get
+ {
+ return MappingPointSnapshot.Create(_root, _anchor.Start,
+ (_trackingMode == SpanTrackingMode.EdgeInclusive ||
+ _trackingMode == SpanTrackingMode.EdgeNegative)
+ ? PointTrackingMode.Negative
+ : PointTrackingMode.Positive,
+ _graph);
+ }
+ }
+
+ public IMappingPoint End
+ {
+ get
+ {
+ return MappingPointSnapshot.Create(_root, _anchor.End,
+ (_trackingMode == SpanTrackingMode.EdgeExclusive ||
+ _trackingMode == SpanTrackingMode.EdgeNegative)
+ ? PointTrackingMode.Negative
+ : PointTrackingMode.Positive,
+ _graph);
+ }
+ }
+
+ public ITextBuffer AnchorBuffer
+ {
+ get { return _anchor.Snapshot.TextBuffer; }
+ }
+
+ public IBufferGraph BufferGraph
+ {
+ get { return _graph; }
+ }
+
+ private void MapUpToBufferNoTrack(ITextBuffer targetBuffer, FrugalList<SnapshotSpan> mappedSpans)
+ {
+ ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer);
+ if (targetSnapshot != null)
+ {
+ //Map _anchor up to targetSnapshot (they should be concurrent snapshots)
+ MapUpToSnapshotNoTrack(targetSnapshot, mappedSpans);
+ }
+ }
+
+ private void MapUpToBufferNoTrack(Predicate<ITextBuffer> match, FrugalList<SnapshotSpan> mappedSpans)
+ {
+ ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, match);
+ if (targetSnapshot != null)
+ {
+ //Map _anchor up to targetSnapshot (they should be concurrent snapshots)
+ MapUpToSnapshotNoTrack(targetSnapshot, mappedSpans);
+ }
+ }
+
+ private void MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, FrugalList<SnapshotSpan> mappedSpans)
+ {
+ MappingSpanSnapshot.MapUpToSnapshotNoTrack(targetSnapshot, _anchor, mappedSpans);
+ }
+
+ public static void MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, SnapshotSpan anchor, IList<SnapshotSpan> mappedSpans)
+ {
+ if (anchor.Snapshot == targetSnapshot)
+ mappedSpans.Add(anchor);
+ else
+ {
+ IProjectionSnapshot targetAsProjection = targetSnapshot as IProjectionSnapshot;
+ if (targetAsProjection != null)
+ {
+ var sourceSnapshots = targetAsProjection.SourceSnapshots;
+ for (int s = 0; s < sourceSnapshots.Count; ++s)
+ {
+ FrugalList<SnapshotSpan> downSpans = new FrugalList<SnapshotSpan>();
+ MapUpToSnapshotNoTrack(sourceSnapshots[s], anchor, downSpans);
+ for (int ds = 0; ds < downSpans.Count; ++ds)
+ {
+ var upSpans = targetAsProjection.MapFromSourceSnapshot(downSpans[ds]);
+ for (int us = 0; us < upSpans.Count; ++us)
+ {
+ mappedSpans.Add(new SnapshotSpan(targetSnapshot, upSpans[us]));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return String.Format("MappingSpanSnapshot anchored at {0}", _anchor);
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs
new file mode 100644
index 0000000..dc61741
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ internal partial class ArrayBuilder<T>
+ {
+ /// <summary>
+ /// struct enumerator used in foreach.
+ /// </summary>
+ internal struct Enumerator : IEnumerator<T>
+ {
+ private readonly ArrayBuilder<T> _builder;
+ private int _index;
+
+ public Enumerator(ArrayBuilder<T> builder)
+ {
+ _builder = builder;
+ _index = -1;
+ }
+
+ public T Current
+ {
+ get
+ {
+ return _builder[_index];
+ }
+ }
+
+ public bool MoveNext()
+ {
+ _index++;
+ return _index < _builder.Count;
+ }
+
+ public void Dispose()
+ {
+ }
+
+ object System.Collections.IEnumerator.Current
+ {
+ get
+ {
+ return this.Current;
+ }
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs
new file mode 100644
index 0000000..7e205b3
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs
@@ -0,0 +1,527 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ [DebuggerDisplay("Count = {Count,nq}")]
+ [DebuggerTypeProxy(typeof(ArrayBuilder<>.DebuggerProxy))]
+ internal sealed partial class ArrayBuilder<T> : IReadOnlyCollection<T>, IReadOnlyList<T>
+ {
+ #region DebuggerProxy
+
+ private sealed class DebuggerProxy
+ {
+ private readonly ArrayBuilder<T> _builder;
+
+ public DebuggerProxy(ArrayBuilder<T> builder)
+ {
+ _builder = builder;
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public T[] A
+ {
+ get
+ {
+ var result = new T[_builder.Count];
+ for (int i = 0; i < result.Length; i++)
+ {
+ result[i] = _builder[i];
+ }
+
+ return result;
+ }
+ }
+ }
+
+ #endregion
+
+ private readonly ImmutableArray<T>.Builder _builder;
+
+ private readonly ObjectPool<ArrayBuilder<T>> _pool;
+
+ public ArrayBuilder(int size)
+ {
+ _builder = ImmutableArray.CreateBuilder<T>(size);
+ }
+
+ public ArrayBuilder() :
+ this(8)
+ { }
+
+ private ArrayBuilder(ObjectPool<ArrayBuilder<T>> pool) :
+ this()
+ {
+ _pool = pool;
+ }
+
+ /// <summary>
+ /// Realizes the array.
+ /// </summary>
+ public ImmutableArray<T> ToImmutable()
+ {
+ return _builder.ToImmutable();
+ }
+
+ public int Count
+ {
+ get
+ {
+ return _builder.Count;
+ }
+ set
+ {
+ _builder.Count = value;
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ return _builder[index];
+ }
+
+ set
+ {
+ _builder[index] = value;
+ }
+ }
+
+ /// <summary>
+ /// Write <paramref name="value"/> to slot <paramref name="index"/>.
+ /// Fills in unallocated slots preceding the <paramref name="index"/>, if any.
+ /// </summary>
+ public void SetItem(int index, T value)
+ {
+ while (index > _builder.Count)
+ {
+ _builder.Add(default(T));
+ }
+
+ if (index == _builder.Count)
+ {
+ _builder.Add(value);
+ }
+ else
+ {
+ _builder[index] = value;
+ }
+ }
+
+ public void Add(T item)
+ {
+ _builder.Add(item);
+ }
+
+ public void Insert(int index, T item)
+ {
+ _builder.Insert(index, item);
+ }
+
+ public void EnsureCapacity(int capacity)
+ {
+ if (_builder.Capacity < capacity)
+ {
+ _builder.Capacity = capacity;
+ }
+ }
+
+ public void Clear()
+ {
+ _builder.Clear();
+ }
+
+ public bool Contains(T item)
+ {
+ return _builder.Contains(item);
+ }
+
+ public int IndexOf(T item)
+ {
+ return _builder.IndexOf(item);
+ }
+
+ public int IndexOf(T item, IEqualityComparer<T> equalityComparer)
+ {
+ return _builder.IndexOf(item, 0, _builder.Count, equalityComparer);
+ }
+
+ public int IndexOf(T item, int startIndex, int count)
+ {
+ return _builder.IndexOf(item, startIndex, count);
+ }
+
+ public int FindIndex(Predicate<T> match)
+ => FindIndex(0, this.Count, match);
+
+ public int FindIndex(int startIndex, Predicate<T> match)
+ => FindIndex(startIndex, this.Count - startIndex, match);
+
+ public int FindIndex(int startIndex, int count, Predicate<T> match)
+ {
+ int endIndex = startIndex + count;
+ for (int i = startIndex; i < endIndex; i++)
+ {
+ if (match(_builder[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public void RemoveAt(int index)
+ {
+ _builder.RemoveAt(index);
+ }
+
+ public void RemoveLast()
+ {
+ _builder.RemoveAt(_builder.Count - 1);
+ }
+
+ public void ReverseContents()
+ {
+ _builder.Reverse();
+ }
+
+ public void Sort()
+ {
+ _builder.Sort();
+ }
+
+ public void Sort(IComparer<T> comparer)
+ {
+ _builder.Sort(comparer);
+ }
+
+ public void Sort(Comparison<T> compare)
+ => Sort(Comparer<T>.Create(compare));
+
+ public void Sort(int startIndex, IComparer<T> comparer)
+ {
+ _builder.Sort(startIndex, _builder.Count - startIndex, comparer);
+ }
+
+ public T[] ToArray()
+ {
+ return _builder.ToArray();
+ }
+
+ public void CopyTo(T[] array, int start)
+ {
+ _builder.CopyTo(array, start);
+ }
+
+ public T Last()
+ {
+ return _builder[_builder.Count - 1];
+ }
+
+ public T First()
+ {
+ return _builder[0];
+ }
+
+ public bool Any()
+ {
+ return _builder.Count > 0;
+ }
+
+ /// <summary>
+ /// Realizes the array.
+ /// </summary>
+ public ImmutableArray<T> ToImmutableOrNull()
+ {
+ if (Count == 0)
+ {
+ return default(ImmutableArray<T>);
+ }
+
+ return this.ToImmutable();
+ }
+
+ /// <summary>
+ /// Realizes the array, downcasting each element to a derived type.
+ /// </summary>
+ public ImmutableArray<U> ToDowncastedImmutable<U>()
+ where U : T
+ {
+ if (Count == 0)
+ {
+ return ImmutableArray<U>.Empty;
+ }
+
+ var tmp = ArrayBuilder<U>.GetInstance(Count);
+ foreach (var i in this)
+ {
+ tmp.Add((U)i);
+ }
+
+ return tmp.ToImmutableAndFree();
+ }
+
+ /// <summary>
+ /// Realizes the array and disposes the builder in one operation.
+ /// </summary>
+ public ImmutableArray<T> ToImmutableAndFree()
+ {
+ var result = this.ToImmutable();
+ this.Free();
+ return result;
+ }
+
+ public T[] ToArrayAndFree()
+ {
+ var result = this.ToArray();
+ this.Free();
+ return result;
+ }
+
+ #region Poolable
+
+ // To implement Poolable, you need two things:
+ // 1) Expose Freeing primitive.
+ public void Free()
+ {
+ var pool = _pool;
+ if (pool != null)
+ {
+ // According to the statistics of a C# compiler self-build, the most commonly used builder size is 0. (808003 uses).
+ // The distant second is the Count == 1 (455619), then 2 (106362) ...
+ // After about 50 (just 67) we have a long tail of infrequently used builder sizes.
+ // However we have builders with size up to 50K (just one such thing)
+ //
+ // We do not want to retain (potentially indefinitely) very large builders
+ // while the chance that we will need their size is diminishingly small.
+ // It makes sense to constrain the size to some "not too small" number.
+ // Overall perf does not seem to be very sensitive to this number, so I picked 128 as a limit.
+ if (this.Count < 128)
+ {
+ if (this.Count != 0)
+ {
+ this.Clear();
+ }
+
+ pool.Free(this);
+ return;
+ }
+ else
+ {
+ pool.ForgetTrackedObject(this);
+ }
+ }
+ }
+
+ // 2) Expose the pool or the way to create a pool or the way to get an instance.
+ // for now we will expose both and figure which way works better
+ private static readonly ObjectPool<ArrayBuilder<T>> s_poolInstance = CreatePool();
+ public static ArrayBuilder<T> GetInstance()
+ {
+ var builder = s_poolInstance.Allocate();
+ Debug.Assert(builder.Count == 0);
+ return builder;
+ }
+
+ public static ArrayBuilder<T> GetInstance(int capacity)
+ {
+ var builder = GetInstance();
+ builder.EnsureCapacity(capacity);
+ return builder;
+ }
+
+ public static ArrayBuilder<T> GetInstance(int capacity, T fillWithValue)
+ {
+ var builder = GetInstance();
+ builder.EnsureCapacity(capacity);
+
+ for (int i = 0; i < capacity; i++)
+ {
+ builder.Add(fillWithValue);
+ }
+
+ return builder;
+ }
+
+ public static ObjectPool<ArrayBuilder<T>> CreatePool()
+ {
+ return CreatePool(128); // we rarely need more than 10
+ }
+
+ public static ObjectPool<ArrayBuilder<T>> CreatePool(int size)
+ {
+ ObjectPool<ArrayBuilder<T>> pool = null;
+ pool = new ObjectPool<ArrayBuilder<T>>(() => new ArrayBuilder<T>(pool), size);
+ return pool;
+ }
+
+ #endregion
+
+ public Enumerator GetEnumerator()
+ {
+ return new Enumerator(this);
+ }
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ internal Dictionary<K, ImmutableArray<T>> ToDictionary<K>(Func<T, K> keySelector, IEqualityComparer<K> comparer = null)
+ {
+ if (this.Count == 1)
+ {
+ var dictionary1 = new Dictionary<K, ImmutableArray<T>>(1, comparer);
+ T value = this[0];
+ dictionary1.Add(keySelector(value), ImmutableArray.Create(value));
+ return dictionary1;
+ }
+
+ if (this.Count == 0)
+ {
+ return new Dictionary<K, ImmutableArray<T>>(comparer);
+ }
+
+ // bucketize
+ // prevent reallocation. it may not have 'count' entries, but it won't have more.
+ var accumulator = new Dictionary<K, ArrayBuilder<T>>(Count, comparer);
+ for (int i = 0; i < Count; i++)
+ {
+ var item = this[i];
+ var key = keySelector(item);
+ if (!accumulator.TryGetValue(key, out var bucket))
+ {
+ bucket = ArrayBuilder<T>.GetInstance();
+ accumulator.Add(key, bucket);
+ }
+
+ bucket.Add(item);
+ }
+
+ var dictionary = new Dictionary<K, ImmutableArray<T>>(accumulator.Count, comparer);
+
+ // freeze
+ foreach (var pair in accumulator)
+ {
+ dictionary.Add(pair.Key, pair.Value.ToImmutableAndFree());
+ }
+
+ return dictionary;
+ }
+
+ public void AddRange(ArrayBuilder<T> items)
+ {
+ _builder.AddRange(items._builder);
+ }
+
+ public void AddRange<U>(ArrayBuilder<U> items) where U : T
+ {
+ _builder.AddRange(items._builder);
+ }
+
+ public void AddRange(ImmutableArray<T> items)
+ {
+ _builder.AddRange(items);
+ }
+
+ public void AddRange(ImmutableArray<T> items, int length)
+ {
+ _builder.AddRange(items, length);
+ }
+
+ public void AddRange<S>(ImmutableArray<S> items) where S : class, T
+ {
+ AddRange(ImmutableArray<T>.CastUp(items));
+ }
+
+ public void AddRange(T[] items, int start, int length)
+ {
+ for (int i = start, end = start + length; i < end; i++)
+ {
+ Add(items[i]);
+ }
+ }
+
+ public void AddRange(IEnumerable<T> items)
+ {
+ _builder.AddRange(items);
+ }
+
+ public void AddRange(params T[] items)
+ {
+ _builder.AddRange(items);
+ }
+
+ public void AddRange(T[] items, int length)
+ {
+ _builder.AddRange(items, length);
+ }
+
+ public void Clip(int limit)
+ {
+ Debug.Assert(limit <= Count);
+ _builder.Count = limit;
+ }
+
+ public void ZeroInit(int count)
+ {
+ _builder.Clear();
+ _builder.Count = count;
+ }
+
+ public void AddMany(T item, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ Add(item);
+ }
+ }
+
+ public void RemoveDuplicates()
+ {
+ var set = PooledHashSet<T>.GetInstance();
+
+ int j = 0;
+ for (int i = 0; i < Count; i++)
+ {
+ if (set.Add(this[i]))
+ {
+ this[j] = this[i];
+ j++;
+ }
+ }
+
+ Clip(j);
+ set.Free();
+ }
+
+ public ImmutableArray<S> SelectDistinct<S>(Func<T, S> selector)
+ {
+ var result = ArrayBuilder<S>.GetInstance(Count);
+ var set = PooledHashSet<S>.GetInstance();
+
+ foreach (var item in this)
+ {
+ var selected = selector(item);
+ if (set.Add(selected))
+ {
+ result.Add(selected);
+ }
+ }
+
+ set.Free();
+ return result.ToImmutableAndFree();
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs
new file mode 100644
index 0000000..2aa6e92
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs
@@ -0,0 +1,279 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+// define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will
+// make everything about 2-3x slower
+//
+// #define TRACE_LEAKS
+
+// define DETECT_LEAKS to detect possible leaks
+// #if DEBUG
+// #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug.
+// #endif
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+#if DETECT_LEAKS
+using System.Runtime.CompilerServices;
+
+#endif
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Generic implementation of object pooling pattern with predefined pool size limit. The main
+ /// purpose is that limited number of frequently used objects can be kept in the pool for
+ /// further recycling.
+ ///
+ /// Notes:
+ /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
+ /// is no space in the pool, extra returned objects will be dropped.
+ ///
+ /// 2) it is implied that if object was obtained from a pool, the caller will return it back in
+ /// a relatively short time. Keeping checked out objects for long durations is ok, but
+ /// reduces usefulness of pooling. Just new up your own.
+ ///
+ /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
+ /// Rationale:
+ /// If there is no intent for reusing the object, do not use pool - just use "new".
+ /// </summary>
+ internal class ObjectPool<T> where T : class
+ {
+ [DebuggerDisplay("{Value,nq}")]
+ private struct Element
+ {
+ internal T Value;
+ }
+
+ /// <remarks>
+ /// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
+ /// which does not have that type (since it compiles against .NET 2.0).
+ /// </remarks>
+ internal delegate T Factory();
+
+ // Storage for the pool objects. The first item is stored in a dedicated field because we
+ // expect to be able to satisfy most requests from it.
+ private T _firstItem;
+ private readonly Element[] _items;
+
+ // factory is stored for the lifetime of the pool. We will call this only when pool needs to
+ // expand. compared to "new T()", Func gives more flexibility to implementers and faster
+ // than "new T()".
+ private readonly Factory _factory;
+
+#if DETECT_LEAKS
+ private static readonly ConditionalWeakTable<T, LeakTracker> leakTrackers = new ConditionalWeakTable<T, LeakTracker>();
+
+ private class LeakTracker : IDisposable
+ {
+ private volatile bool disposed;
+
+#if TRACE_LEAKS
+ internal volatile object Trace = null;
+#endif
+
+ public void Dispose()
+ {
+ disposed = true;
+ GC.SuppressFinalize(this);
+ }
+
+ private string GetTrace()
+ {
+#if TRACE_LEAKS
+ return Trace == null ? "" : Trace.ToString();
+#else
+ return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n";
+#endif
+ }
+
+ ~LeakTracker()
+ {
+ if (!this.disposed && !Environment.HasShutdownStarted)
+ {
+ var trace = GetTrace();
+
+ // If you are seeing this message it means that object has been allocated from the pool
+ // and has not been returned back. This is not critical, but turns pool into rather
+ // inefficient kind of "new".
+ Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END");
+ }
+ }
+ }
+#endif
+
+ internal ObjectPool(Factory factory)
+ : this(factory, Environment.ProcessorCount * 2)
+ { }
+
+ internal ObjectPool(Factory factory, int size)
+ {
+ Debug.Assert(size >= 1);
+ _factory = factory;
+ _items = new Element[size - 1];
+ }
+
+ private T CreateInstance()
+ {
+ var inst = _factory();
+ return inst;
+ }
+
+ /// <summary>
+ /// Produces an instance.
+ /// </summary>
+ /// <remarks>
+ /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
+ /// Note that Free will try to store recycled objects close to the start thus statistically
+ /// reducing how far we will typically search.
+ /// </remarks>
+ internal T Allocate()
+ {
+ // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
+ // Note that the initial read is optimistically not synchronized. That is intentional.
+ // We will interlock only when we have a candidate. in a worst case we may miss some
+ // recently returned objects. Not a big deal.
+ T inst = _firstItem;
+ if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
+ {
+ inst = AllocateSlow();
+ }
+
+#if DETECT_LEAKS
+ var tracker = new LeakTracker();
+ leakTrackers.Add(inst, tracker);
+
+#if TRACE_LEAKS
+ var frame = CaptureStackTrace();
+ tracker.Trace = frame;
+#endif
+#endif
+ return inst;
+ }
+
+ private T AllocateSlow()
+ {
+ var items = _items;
+
+ for (int i = 0; i < items.Length; i++)
+ {
+ // Note that the initial read is optimistically not synchronized. That is intentional.
+ // We will interlock only when we have a candidate. in a worst case we may miss some
+ // recently returned objects. Not a big deal.
+ T inst = items[i].Value;
+ if (inst != null)
+ {
+ if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst))
+ {
+ return inst;
+ }
+ }
+ }
+
+ return CreateInstance();
+ }
+
+ /// <summary>
+ /// Returns objects to the pool.
+ /// </summary>
+ /// <remarks>
+ /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
+ /// Note that Free will try to store recycled objects close to the start thus statistically
+ /// reducing how far we will typically search in Allocate.
+ /// </remarks>
+ internal void Free(T obj)
+ {
+ Validate(obj);
+ ForgetTrackedObject(obj);
+
+ if (_firstItem == null)
+ {
+ // Intentionally not using interlocked here.
+ // In a worst case scenario two objects may be stored into same slot.
+ // It is very unlikely to happen and will only mean that one of the objects will get collected.
+ _firstItem = obj;
+ }
+ else
+ {
+ FreeSlow(obj);
+ }
+ }
+
+ private void FreeSlow(T obj)
+ {
+ var items = _items;
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (items[i].Value == null)
+ {
+ // Intentionally not using interlocked here.
+ // In a worst case scenario two objects may be stored into same slot.
+ // It is very unlikely to happen and will only mean that one of the objects will get collected.
+ items[i].Value = obj;
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes an object from leak tracking.
+ ///
+ /// This is called when an object is returned to the pool. It may also be explicitly
+ /// called if an object allocated from the pool is intentionally not being returned
+ /// to the pool. This can be of use with pooled arrays if the consumer wants to
+ /// return a larger array to the pool than was originally allocated.
+ /// </summary>
+ [Conditional("DEBUG")]
+ internal void ForgetTrackedObject(T old, T replacement = null)
+ {
+#if DETECT_LEAKS
+ LeakTracker tracker;
+ if (leakTrackers.TryGetValue(old, out tracker))
+ {
+ tracker.Dispose();
+ leakTrackers.Remove(old);
+ }
+ else
+ {
+ var trace = CaptureStackTrace();
+ Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END");
+ }
+
+ if (replacement != null)
+ {
+ tracker = new LeakTracker();
+ leakTrackers.Add(replacement, tracker);
+ }
+#endif
+ }
+
+#if DETECT_LEAKS
+ private static Lazy<Type> _stackTraceType = new Lazy<Type>(() => Type.GetType("System.Diagnostics.StackTrace"));
+
+ private static object CaptureStackTrace()
+ {
+ return Activator.CreateInstance(_stackTraceType.Value);
+ }
+#endif
+
+ [Conditional("DEBUG")]
+ private void Validate(object obj)
+ {
+ Debug.Assert(obj != null, "freeing null?");
+
+ Debug.Assert(_firstItem != obj, "freeing twice?");
+
+ var items = _items;
+ for (int i = 0; i < items.Length; i++)
+ {
+ var value = items[i].Value;
+ if (value == null)
+ {
+ return;
+ }
+
+ Debug.Assert(value != obj, "freeing twice?");
+ }
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs
new file mode 100644
index 0000000..a98addd
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ // Dictionary that can be recycled via an object pool
+ // NOTE: these dictionaries always have the default comparer.
+ internal class PooledDictionary<K, V> : Dictionary<K, V>
+ {
+ private readonly ObjectPool<PooledDictionary<K, V>> _pool;
+
+ private PooledDictionary(ObjectPool<PooledDictionary<K, V>> pool)
+ {
+ _pool = pool;
+ }
+
+ public ImmutableDictionary<K, V> ToImmutableDictionaryAndFree()
+ {
+ var result = this.ToImmutableDictionary();
+ this.Free();
+ return result;
+ }
+
+ public void Free()
+ {
+ this.Clear();
+ _pool?.Free(this);
+ }
+
+ // global pool
+ private static readonly ObjectPool<PooledDictionary<K, V>> s_poolInstance = CreatePool();
+
+ // if someone needs to create a pool;
+ public static ObjectPool<PooledDictionary<K, V>> CreatePool()
+ {
+ ObjectPool<PooledDictionary<K, V>> pool = null;
+ pool = new ObjectPool<PooledDictionary<K, V>>(() => new PooledDictionary<K, V>(pool), 128);
+ return pool;
+ }
+
+ public static PooledDictionary<K, V> GetInstance()
+ {
+ var instance = s_poolInstance.Allocate();
+ Debug.Assert(instance.Count == 0);
+ return instance;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs
new file mode 100644
index 0000000..2969170
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ // HashSet that can be recycled via an object pool
+ // NOTE: these HashSets always have the default comparer.
+ internal class PooledHashSet<T> : HashSet<T>
+ {
+ private readonly ObjectPool<PooledHashSet<T>> _pool;
+
+ private PooledHashSet(ObjectPool<PooledHashSet<T>> pool)
+ {
+ _pool = pool;
+ }
+
+ public void Free()
+ {
+ this.Clear();
+ _pool?.Free(this);
+ }
+
+ // global pool
+ private static readonly ObjectPool<PooledHashSet<T>> s_poolInstance = CreatePool();
+
+ // if someone needs to create a pool;
+ public static ObjectPool<PooledHashSet<T>> CreatePool()
+ {
+ ObjectPool<PooledHashSet<T>> pool = null;
+ pool = new ObjectPool<PooledHashSet<T>>(() => new PooledHashSet<T>(pool), 128);
+ return pool;
+ }
+
+ public static PooledHashSet<T> GetInstance()
+ {
+ var instance = s_poolInstance.Allocate();
+ Debug.Assert(instance.Count == 0);
+ return instance;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs
new file mode 100644
index 0000000..b618095
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Diagnostics;
+using System.Text;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// The usage is:
+ /// var inst = PooledStringBuilder.GetInstance();
+ /// var sb = inst.builder;
+ /// ... Do Stuff...
+ /// ... sb.ToString() ...
+ /// inst.Free();
+ /// </summary>
+ internal class PooledStringBuilder
+ {
+ public readonly StringBuilder Builder = new StringBuilder();
+ private readonly ObjectPool<PooledStringBuilder> _pool;
+
+ private PooledStringBuilder(ObjectPool<PooledStringBuilder> pool)
+ {
+ Debug.Assert(pool != null);
+ _pool = pool;
+ }
+
+ public int Length
+ {
+ get { return this.Builder.Length; }
+ }
+
+ public void Free()
+ {
+ var builder = this.Builder;
+
+ // do not store builders that are too large.
+ if (builder.Capacity <= 1024)
+ {
+ builder.Clear();
+ _pool.Free(this);
+ }
+ else
+ {
+ _pool.ForgetTrackedObject(this);
+ }
+ }
+
+ [System.Obsolete("Consider calling ToStringAndFree instead.")]
+ public new string ToString()
+ {
+ return this.Builder.ToString();
+ }
+
+ public string ToStringAndFree()
+ {
+ string result = this.Builder.ToString();
+ this.Free();
+
+ return result;
+ }
+
+ public string ToStringAndFree(int startIndex, int length)
+ {
+ string result = this.Builder.ToString(startIndex, length);
+ this.Free();
+
+ return result;
+ }
+
+ // global pool
+ private static readonly ObjectPool<PooledStringBuilder> s_poolInstance = CreatePool();
+
+ // if someone needs to create a private pool;
+ public static ObjectPool<PooledStringBuilder> CreatePool()
+ {
+ ObjectPool<PooledStringBuilder> pool = null;
+ pool = new ObjectPool<PooledStringBuilder>(() => new PooledStringBuilder(pool), 32);
+ return pool;
+ }
+
+ public static PooledStringBuilder GetInstance()
+ {
+ var builder = s_poolInstance.Allocate();
+ Debug.Assert(builder.Builder.Length == 0);
+ return builder;
+ }
+
+ public static implicit operator StringBuilder(PooledStringBuilder obj)
+ {
+ return obj.Builder;
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs
new file mode 100644
index 0000000..951cf14
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs
@@ -0,0 +1,287 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using Microsoft.VisualStudio.Text.Differencing;
+ using Microsoft.VisualStudio.Text.Projection;
+
+ internal class ProjectionSpanDiffer
+ {
+ private IDifferenceService diffService;
+
+ private ReadOnlyCollection<SnapshotSpan> inputDeletedSnapSpans;
+ private ReadOnlyCollection<SnapshotSpan> inputInsertedSnapSpans;
+ private bool computed;
+
+ // exposed to unit tests
+ internal List<SnapshotSpan>[] deletedSurrogates;
+ internal List<SnapshotSpan>[] insertedSurrogates;
+
+ public static ProjectionSpanDifference DiffSourceSpans(IDifferenceService diffService,
+ IProjectionSnapshot left,
+ IProjectionSnapshot right)
+ {
+ if (left == null)
+ {
+ throw new ArgumentNullException("left");
+ }
+ if (right == null)
+ {
+ throw new ArgumentNullException("right");
+ }
+
+ if (!object.ReferenceEquals(left.TextBuffer, right.TextBuffer))
+ {
+ throw new ArgumentException("left does not belong to the same text buffer as right");
+ }
+
+ ProjectionSpanDiffer differ = new ProjectionSpanDiffer
+ (diffService,
+ left.GetSourceSpans(),
+ right.GetSourceSpans());
+
+ return new ProjectionSpanDifference(differ.GetDifferences(), differ.InsertedSpans, differ.DeletedSpans);
+ }
+
+ public ProjectionSpanDiffer(IDifferenceService diffService,
+ ReadOnlyCollection<SnapshotSpan> deletedSnapSpans,
+ ReadOnlyCollection<SnapshotSpan> insertedSnapSpans)
+ {
+ this.diffService = diffService;
+ this.inputDeletedSnapSpans = deletedSnapSpans;
+ this.inputInsertedSnapSpans = insertedSnapSpans;
+ }
+
+ public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; }
+ public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; }
+
+ public IDifferenceCollection<SnapshotSpan> GetDifferences()
+ {
+ if (!this.computed)
+ {
+ DecomposeSpans();
+ this.computed = true;
+ }
+
+ var deletedSpans = new List<SnapshotSpan>();
+ var insertedSpans = new List<SnapshotSpan>();
+
+ DeletedSpans = deletedSpans.AsReadOnly();
+ InsertedSpans = insertedSpans.AsReadOnly();
+
+ for (int s = 0; s < deletedSurrogates.Length; ++s)
+ {
+ deletedSpans.AddRange(deletedSurrogates[s]);
+ }
+
+ for (int s = 0; s < insertedSurrogates.Length; ++s)
+ {
+ insertedSpans.AddRange(insertedSurrogates[s]);
+ }
+
+ return this.diffService.DifferenceSequences(deletedSpans, insertedSpans);
+ }
+
+ #region Internal (for testing) helpers
+ internal void DecomposeSpans()
+ {
+ // Prepare spans for diffing. The basic idea is this: suppose we have input spans from some source snapshot as follows:
+ //
+ // deleted: 0..10
+ // inserted: 0..3 7..10
+ //
+ // We would like to raise a text change event that indicates that the text from 3..7 was deleted, rather than
+ // an event indicating that all the text from 0..10 was deleted and replaced. We could simply compute a string
+ // difference of the before & after text, but there might be a lot of text (so that would be expensive), and we
+ // also don't want to suppress eventing when identical text comes from different source buffers (which might have
+ // different content types). So, this routine converts the input spans into a form suitable for diffing:
+ //
+ // deleted: 0..3 3..7 7..10
+ // inserted 0..3 7..10
+ //
+ // then we compute the differences of the spans qua spans, which in this case will indicate that the span named "3..7"
+ // was deleted, and that's what we use to generate text change events.
+
+ // what to substitute for input spans during diffing
+ this.deletedSurrogates = new List<SnapshotSpan>[this.inputDeletedSnapSpans.Count];
+ this.insertedSurrogates = new List<SnapshotSpan>[this.inputInsertedSnapSpans.Count];
+
+ // collect spans by text buffer
+
+ Dictionary<ITextSnapshot, List<Thing>> buffer2DeletedThings = new Dictionary<ITextSnapshot, List<Thing>>();
+ for (int ds = 0; ds < this.inputDeletedSnapSpans.Count; ++ds)
+ {
+ SnapshotSpan ss = this.inputDeletedSnapSpans[ds];
+ List<Thing> things;
+ if (!buffer2DeletedThings.TryGetValue(ss.Snapshot, out things))
+ {
+ things = new List<Thing>();
+ buffer2DeletedThings.Add(ss.Snapshot, things);
+ }
+ things.Add(new Thing(ss.Span, ds));
+
+ // unrelated
+ deletedSurrogates[ds] = new List<SnapshotSpan>();
+ }
+
+ Dictionary<ITextSnapshot, List<Thing>> buffer2InsertedThings = new Dictionary<ITextSnapshot, List<Thing>>();
+ for (int ns = 0; ns < this.inputInsertedSnapSpans.Count; ++ns)
+ {
+ SnapshotSpan ss = this.inputInsertedSnapSpans[ns];
+ List<Thing> things;
+ if (!buffer2InsertedThings.TryGetValue(ss.Snapshot, out things))
+ {
+ things = new List<Thing>();
+ buffer2InsertedThings.Add(ss.Snapshot, things);
+ }
+ things.Add(new Thing(ss.Span, ns));
+
+ // unrelated
+ insertedSurrogates[ns] = new List<SnapshotSpan>();
+ }
+
+ foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2DeletedThings)
+ {
+ List<Thing> insertedThings;
+ ITextSnapshot snapshot = pair.Key;
+ if (buffer2InsertedThings.TryGetValue(snapshot, out insertedThings))
+ {
+ List<Thing> deletedThings = pair.Value;
+ insertedThings.Sort(Comparison);
+ deletedThings.Sort(Comparison);
+
+ int i = 0;
+ int d = 0;
+ do
+ {
+ Span inserted = insertedThings[i].span;
+ Span deleted = deletedThings[d].span;
+ Span? overlap = inserted.Overlap(deleted);
+
+ if (overlap == null)
+ {
+ if (inserted.Start < deleted.Start)
+ {
+ i++;
+ }
+ else
+ {
+ d++;
+ }
+ }
+ else
+ {
+ NormalizedSpanCollection insertedResidue = NormalizedSpanCollection.Difference(new NormalizedSpanCollection(inserted),
+ new NormalizedSpanCollection(overlap.Value)); // todo add overload to normalizedspancollection
+ if (insertedResidue.Count > 0)
+ {
+ int pos = insertedThings[i].position;
+ insertedThings.RemoveAt(i);
+ bool didOverlap = false;
+ int ir = 0;
+ while (ir < insertedResidue.Count)
+ {
+ Span r = insertedResidue[ir];
+ if (didOverlap || r.Start < overlap.Value.Start)
+ {
+ insertedThings.Insert(i++, new Thing(r, pos));
+ ir++;
+ }
+ else
+ {
+ insertedThings.Insert(i++, new Thing(overlap.Value, pos));
+ didOverlap = true;
+ }
+ }
+ if (!didOverlap)
+ {
+ insertedThings.Insert(i++, new Thing(overlap.Value, pos));
+ }
+ i--;
+ }
+
+ NormalizedSpanCollection deletedResidue = NormalizedSpanCollection.Difference(new NormalizedSpanCollection(deleted),
+ new NormalizedSpanCollection(overlap.Value));
+ if (deletedResidue.Count > 0)
+ {
+ int pos = deletedThings[d].position;
+ deletedThings.RemoveAt(d);
+ bool didOverlap = false;
+ int dr = 0;
+ while (dr < deletedResidue.Count)
+ {
+ Span r = deletedResidue[dr];
+ if (didOverlap || r.Start < overlap.Value.Start)
+ {
+ deletedThings.Insert(d++, new Thing(r, pos));
+ dr++;
+ }
+ else
+ {
+ deletedThings.Insert(d++, new Thing(overlap.Value, pos));
+ didOverlap = true;
+ }
+ }
+ if (!didOverlap)
+ {
+ deletedThings.Insert(d++, new Thing(overlap.Value, pos));
+ }
+ d--;
+ }
+ }
+ if (inserted.End <= deleted.End)
+ {
+ i++;
+ }
+ if (deleted.End <= inserted.End)
+ {
+ d++;
+ }
+ } while (i < insertedThings.Count && d < deletedThings.Count);
+ }
+ }
+
+ foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2DeletedThings)
+ {
+ foreach (Thing t in pair.Value)
+ {
+ deletedSurrogates[t.position].Add(new SnapshotSpan(pair.Key, t.span));
+ }
+ }
+
+ foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2InsertedThings)
+ {
+ foreach (Thing t in pair.Value)
+ {
+ insertedSurrogates[t.position].Add(new SnapshotSpan(pair.Key, t.span));
+ }
+ }
+ }
+
+ #endregion
+
+ #region Private helpers
+ private class Thing
+ {
+ public Span span;
+ public int position;
+ public Thing(Span span, int position)
+ {
+ this.span = span;
+ this.position = position;
+ }
+ }
+
+ private int Comparison(Thing left, Thing right)
+ {
+ return left.span.Start - right.span.Start;
+ }
+ #endregion
+
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs
new file mode 100644
index 0000000..99d3267
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs
@@ -0,0 +1,43 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System.Collections.ObjectModel;
+using Microsoft.VisualStudio.Text.Differencing;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Represents the set of differences between two projection snapshots.
+ /// </summary>
+ public class ProjectionSpanDifference
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="ProjectionSpanDifference"/>.
+ /// </summary>
+ /// <param name="differenceCollection">The collection of snapshot spans that include the differences.</param>
+ /// <param name="insertedSpans">A read-only collection of the inserted snapshot spans.</param>
+ /// <param name="deletedSpans">A read-only collection of the deleted snapshot spans.</param>
+ public ProjectionSpanDifference(IDifferenceCollection<SnapshotSpan> differenceCollection, ReadOnlyCollection<SnapshotSpan> insertedSpans, ReadOnlyCollection<SnapshotSpan> deletedSpans)
+ {
+ DifferenceCollection = differenceCollection;
+ InsertedSpans = insertedSpans;
+ DeletedSpans = deletedSpans;
+ }
+
+ /// <summary>
+ /// The collection of differences between the two snapshots.
+ /// </summary>
+ public IDifferenceCollection<SnapshotSpan> DifferenceCollection { get; private set; }
+
+ /// <summary>
+ /// The read-only collection of inserted snapshot spans.
+ /// </summary>
+ public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; }
+
+ /// <summary>
+ /// The read-only collection of deleted snapshot spans.
+ /// </summary>
+ public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/SnapshotTracker.cs b/src/Text/Util/TextDataUtil/SnapshotTracker.cs
new file mode 100644
index 0000000..e89bf02
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/SnapshotTracker.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// Tracks the extant snapshots of a text buffer for diagnostic purposes.
+ /// </summary>
+ internal sealed class SnapshotTracker : IDisposable
+ {
+ private readonly ITextBuffer textBuffer;
+ List<WeakReference> snapshots = new List<WeakReference>();
+
+ public SnapshotTracker(ITextBuffer textBuffer)
+ {
+ this.textBuffer = textBuffer;
+ this.snapshots.Add(new WeakReference(textBuffer.CurrentSnapshot));
+ textBuffer.ChangedLowPriority += OnTextContentChanged;
+ }
+
+ public void Dispose()
+ {
+ this.textBuffer.ChangedLowPriority -= OnTextContentChanged;
+ GC.SuppressFinalize(this);
+ }
+
+ private void OnTextContentChanged(object sender, TextContentChangedEventArgs e)
+ {
+ this.snapshots.Add(new WeakReference(e.After));
+ }
+
+ public int ReportLiveSnapshots(System.IO.TextWriter writer)
+ {
+ int liveSnapshots = 0;
+ writer.Write(" Snapshots: ");
+ for (int s = 0; s < this.snapshots.Count; ++s)
+ {
+ object target = snapshots[s].Target;
+ if (target != null)
+ {
+ liveSnapshots++;
+ ITextVersion v = ((ITextSnapshot)target).Version;
+ writer.Write(string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0}:{1} ", v.VersionNumber, v.Length));
+ }
+ }
+ writer.WriteLine();
+ return liveSnapshots;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Util/TextDataUtil/TextModelOptions.cs b/src/Text/Util/TextDataUtil/TextModelOptions.cs
new file mode 100644
index 0000000..1863bfd
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/TextModelOptions.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ public static class TextModelOptions
+ {
+ // these values should be set by MEF component in editor options component. defaults
+ // are here just in case that doesn't happen in some configuration.
+ public static int CompressedStorageFileSizeThreshold = 5 * 1024 * 1024; // 5 MB file (typically 10 MB in memory)
+ public static int CompressedStoragePageSize = 1 * 1024 * 1024; // 1 MB per page (so 10 pages at the low end)
+ public static int CompressedStorageMaxLoadedPages = 3; // at most 3 pages loaded
+ public static bool CompressedStorageGlobalManagement = false; // per document
+ public static bool CompressedStorageRetainWeakReferences = true; // forces worst case decompression for testing purposes
+
+ public static int StringRebuilderMaxCharactersToConsolidate = 200; // Combine adjacent pieces when sum of sizes is less than this and
+ public static int StringRebuilderMaxLinesToConsolidate = 8; // Combine adjacent pieces when number of lines is less than this
+
+ public static int DiffSizeThreshold = 25 * 1024 * 1024; // threshold above which to do poor man's diff
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/TextUtilities.cs b/src/Text/Util/TextDataUtil/TextUtilities.cs
new file mode 100644
index 0000000..26aa19a
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/TextUtilities.cs
@@ -0,0 +1,438 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using System.Diagnostics;
+ using System.Threading.Tasks;
+
+ /// <summary>
+ /// Handy text-oriented utilities.
+ /// </summary>
+ internal static class TextUtilities
+ {
+ public static readonly Task CompletedNonInliningTask = CreateCompletedNonInliningTask();
+
+ private static Task CreateCompletedNonInliningTask()
+ {
+ var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
+ tcs.SetResult(null);
+ return tcs.Task;
+ }
+
+ public static Span? Overlap(this Span span, Span? otherSpan)
+ {
+ return otherSpan != null ? span.Overlap(otherSpan.Value) : null;
+ }
+
+ public static int LengthOfLineBreak(string source, int start, int end)
+ {
+ char c1 = source[start];
+ if (c1 == '\r')
+ {
+ return ((++start < end) && (source[start] == '\n')) ? 2 : 1;
+ }
+ else if ((c1 == '\n') || (c1 == '\u0085') ||
+ (c1 == '\u2028' /*unicode line separator*/) ||
+ (c1 == '\u2029' /*unicode paragraph separator*/))
+ {
+ return 1;
+ }
+ else
+ return 0;
+ }
+
+ public static int LengthOfLineBreak(char[] source, int start, int end)
+ {
+ char c1 = source[start];
+ if (c1 == '\r')
+ {
+ return ((++start < end) && (source[start] == '\n')) ? 2 : 1;
+ }
+ else if ((c1 == '\n') || (c1 == '\u0085') ||
+ (c1 == '\u2028' /*unicode line separator*/) ||
+ (c1 == '\u2029' /*unicode paragraph separator*/))
+ {
+ return 1;
+ }
+ else
+ return 0;
+ }
+
+ /// <summary>
+ /// A utility function that returns the number of lines in a given block of
+ /// text
+ ///
+ /// We honor the MAC, UNIX and PC way of line counting.
+ ///
+ /// 0x2028, 0x2929, 0x85, \r, \n and \r\n are considered as valid line breaks. However, \n\r is not
+ /// (that is to say, \n\r comprises TWO line breaks).
+ /// </summary>
+ static public int ScanForLineCount(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+ int lines = 0;
+ for (int i = 0; i < text.Length; )
+ {
+ int breakLength = LengthOfLineBreak(text, i, text.Length);
+ if (breakLength > 0)
+ {
+ ++lines;
+ i += breakLength;
+ }
+ else
+ ++i;
+ }
+
+ return lines;
+ }
+
+ /// <summary>
+ /// A utility function that returns the number of lines in a given block of text.
+ ///
+ /// We honor the MAC, UNIX and PC way of line counting.
+ ///
+ /// 0x2028, 0x2929, 0x85, \r, \n and \r\n are considered as valid line breaks. However, \n\r is not
+ /// (that is to say, \n\r comprises TWO line breaks).
+ /// </summary>
+ static public int ScanForLineCount(char[] text, int start, int length)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+ int lines = 0;
+ for (int i = 0; i < length; )
+ {
+ int breakLength = LengthOfLineBreak(text, start + i, length);
+ if (breakLength > 0)
+ {
+ ++lines;
+ i += breakLength;
+ }
+ else
+ ++i;
+ }
+
+ return lines;
+ }
+
+ /// <summary>
+ /// Perform stable merge sort on <paramref name="elements"/>.
+ /// </summary>
+ /// <param name="elements">List of elements to be sorted.</param>
+ /// <param name="comparer">He who knows how to compare elements.</param>
+ /// <returns>Array of parameter T sorted in ascending order.</returns>
+ static public T[] StableSort<T>(IReadOnlyList<T> elements, Comparison<T> comparer)
+ {
+ int count = elements.Count;
+ T[] target = new T[count];
+ bool alreadySorted = true;
+ if (count > 0)
+ {
+ target[0] = elements[0];
+ for (int i = 1; i < count; ++i)
+ {
+ target[i] = elements[i];
+ // check for already-sorted array; quite likely in applicable text editor scenarios
+ if (comparer(target[i - 1], target[i]) > 0)
+ {
+ alreadySorted = false;
+ }
+ }
+ }
+ if (alreadySorted)
+ {
+ return target;
+ }
+ else
+ {
+ // do a merge sort
+
+ T[] source = new T[count];
+ int subcount = 1; // the length of the subsequences to merge
+ while (subcount < count)
+ {
+ T[] t = source;
+ source = target;
+ target = t;
+
+ MergePass(source, target, subcount, comparer);
+
+ subcount *= 2;
+ }
+#if DEBUG
+ // trust, but verify
+ for (int i = 1; i < count; ++i)
+ {
+ System.Diagnostics.Debug.Assert(comparer(target[i - 1], target[i]) <= 0);
+ }
+#endif
+ return target;
+ }
+ }
+
+ /// <summary>
+ /// Make one pass over <paramref name="source"/>, merging runs of size <paramref name="subcount"/> into <paramref name="target"/>.
+ /// </summary>
+ /// <param name="source">Array containing sorted runs of size <paramref name="subcount"/>.</param>
+ /// <param name="target">Array into which runs of size 2 * <paramref name="subcount"/> will be placed.</param>
+ /// <param name="subcount">Size of sorted runs that will be generated into <paramref name="target"/>.</param>
+ /// <param name="comparer">He who knows how to compare elements.</param>
+ static private void MergePass<T>(T[] source, T[] target, int subcount, Comparison<T> comparer)
+ {
+ int offset = 0; // offset of the pair of subsequences that are being merged
+ while (offset <= source.Length - 2 * subcount)
+ {
+ Merge(source, target, offset, offset + subcount, offset + 2 * subcount, comparer);
+ offset += 2 * subcount;
+ }
+ if (offset + subcount < source.Length)
+ {
+ // two subsequences remain, but the second one is smaller than subcount
+ Merge(source, target, offset, offset + subcount, source.Length, comparer);
+ }
+ else
+ {
+ // only one (already sorted) subsequence remains; copy it across
+ for (int i = offset; i < source.Length; ++i)
+ {
+ target[i] = source[i];
+ }
+ }
+ }
+
+ /// <summary>
+ /// Merge two ordered runs from <paramref name="source"/> array into <paramref name="target"/> array.
+ /// </summary>
+ /// <param name="source">Array containing input runs.</param>
+ /// <param name="target">Array into which output run is placed.</param>
+ /// <param name="left">Starting position of first source run.</param>
+ /// <param name="right">Starting position of second source run.</param>
+ /// <param name="end">First position past second source run.</param>
+ /// <param name="comparer">He who knows how to compare elements.</param>
+ static private void Merge<T>(T[] source, T[] target, int left, int right, int end, Comparison<T> comparer)
+ {
+ int s1 = left;
+ int s2 = right;
+ int t = left;
+ while (s1 < right && s2 < end)
+ {
+ target[t++] = (comparer(source[s1], source[s2]) <= 0) ? source[s1++] : source[s2++];
+ }
+ if (s1 == right)
+ {
+ // copy any leftovers from second source run.
+ for (int i = t; i < end; ++i)
+ {
+ target[i] = source[s2++];
+ }
+ }
+ else
+ {
+ // copy any leftovers from first source run.
+ for (int i = t; i < end; ++i)
+ {
+ target[i] = source[s1++];
+ }
+ }
+ }
+
+ /// <summary>
+ /// Associate a string-valued tag with the buffer for diagnostic purposes.
+ /// </summary>
+ /// <param name="buffer">The <see cref="ITextBuffer"/> of interest.</param>
+ /// <param name="tag">Informative tag to associate with the buffer.</param>
+ /// <exception cref="ArgumentNullException">if <paramref name="buffer"/> is null.</exception>
+ /// <exception cref="ArgumentNullException">if <paramref name="tag"/> is null.</exception>
+ /// <remarks>If the buffer already has a tag, the new tag is concatenated to it (with an intervening "::".</remarks>
+ public static void TagBuffer(ITextBuffer buffer, string tag)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+ if (tag == null)
+ {
+ throw new ArgumentNullException("tag");
+ }
+ string existingTag = "";
+ if (!buffer.Properties.TryGetProperty<string>("tag", out existingTag))
+ {
+ buffer.Properties.AddProperty("tag", tag);
+ }
+ else
+ {
+ buffer.Properties["tag"] = existingTag + "::" + tag;
+ }
+ }
+
+ /// <summary>
+ /// Return the string-valued tag previously associated with the buffer by <see cref="TagBuffer"/>.
+ /// </summary>
+ /// <param name="buffer">The <see cref="ITextBuffer"/> of interest.</param>
+ /// <returns>The previously associated tag, or "" if no tag has been associated.</returns>
+ /// <exception cref="ArgumentNullException">if <paramref name="buffer"/> is null.</exception>
+ public static string GetTag(ITextBuffer buffer)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+ string tag = "";
+ buffer.Properties.TryGetProperty<string>("tag", out tag);
+ return tag;
+ }
+
+ public static string GetTagOrContentType(ITextBuffer buffer)
+ {
+ return GetTag(buffer) ?? buffer.ContentType.TypeName;
+ }
+
+ public static string Escape(string text)
+ {
+ StringBuilder result = new StringBuilder();
+ for (int c = 0; c < text.Length; ++c)
+ {
+ char ch = text[c];
+ AppendChar(result, ch);
+ }
+ return result.ToString();
+ }
+
+ public static string Escape(string text, int truncateLength)
+ {
+ StringBuilder result = new StringBuilder();
+ if (text.Length <= truncateLength)
+ {
+ return Escape(text);
+ }
+ else
+ {
+ for (int c = 0; c < truncateLength / 2; ++c)
+ {
+ char ch = text[c];
+ AppendChar(result, ch);
+ }
+ result.Append(" � ");
+ for (int c = text.Length - (truncateLength / 2); c < text.Length; ++c)
+ {
+ char ch = text[c];
+ AppendChar(result, ch);
+ }
+ }
+ return result.ToString();
+ }
+
+ private static void AppendChar(StringBuilder result, char ch)
+ {
+ switch (ch)
+ {
+ case '\\':
+ result.Append(@"\\");
+ break;
+ case '\r':
+ result.Append(@"\r");
+ break;
+ case '\n':
+ result.Append(@"\n");
+ break;
+ case '\t':
+ result.Append(@"\t");
+ break;
+ case '"':
+ result.Append("\\\"");
+ break;
+ default:
+ result.Append(ch);
+ break;
+ }
+ }
+
+ internal static void RaiseEvent(object sender, EventHandler eventHandlers, IEnumerable<IExtensionErrorHandler> errorHandlers)
+ {
+ if (eventHandlers == null)
+ return;
+
+ var handlers = eventHandlers.GetInvocationList();
+
+ foreach (EventHandler handler in handlers)
+ {
+ try
+ {
+ handler(sender, EventArgs.Empty);
+ }
+ catch (Exception e)
+ {
+ HandleException(sender, e, errorHandlers);
+ }
+ }
+ }
+
+ internal static void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args,
+ IEnumerable<IExtensionErrorHandler> errorHandlers) where TArgs : EventArgs
+ {
+ if (eventHandlers == null)
+ return;
+
+ var handlers = eventHandlers.GetInvocationList();
+
+ foreach (EventHandler<TArgs> handler in handlers)
+ {
+ try
+ {
+ handler(sender, args);
+ }
+ catch (Exception e)
+ {
+ HandleException(sender, e, errorHandlers);
+ }
+ }
+ }
+
+ internal static void CallExtensionPoint(object errorSource, Action call, IEnumerable<IExtensionErrorHandler> errorHandlers)
+ {
+ try
+ {
+ call();
+ }
+ catch (Exception e)
+ {
+ HandleException(errorSource, e, errorHandlers);
+ }
+ }
+
+ internal static void HandleException(object errorSource, Exception e, IEnumerable<IExtensionErrorHandler> errorHandlers)
+ {
+ bool handled = false;
+ if (errorHandlers != null)
+ {
+ foreach (var errorHandler in errorHandlers)
+ {
+ try
+ {
+ errorHandler.HandleError(errorSource, e);
+ handled = true;
+ }
+ catch (Exception doubleFaultException)
+ {
+ // TODO: What is the right behavior here?
+ Debug.Fail(doubleFaultException.ToString());
+ }
+ }
+ }
+ if (!handled)
+ {
+ // TODO: What is the right behavior here?
+ Debug.Fail(e.ToString());
+ }
+ }
+ }
+}
diff --git a/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs b/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs
new file mode 100644
index 0000000..550218c
--- /dev/null
+++ b/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs
@@ -0,0 +1,102 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// This class is the same as WeakReference, except for its implementation of Object.GetHashCode() and Object.Equals().
+ /// In both of those cases, it makes a best attempt at emulating the behavior of the target object, so that when
+ /// WeakReferences are placed within a dictionary, as key values, they are properly recognized as references to objects
+ /// that are the different (or the same).
+ /// </summary>
+ [Serializable]
+ public class WeakReferenceForDictionaryKey : WeakReference
+ {
+ private readonly int hashCode;
+ private const int xorWeakReference = 0xCCCC;
+
+ public WeakReferenceForDictionaryKey(object target)
+ : base(target)
+ {
+ if (target != null)
+ hashCode = target.GetHashCode() ^ xorWeakReference;
+ }
+
+ protected WeakReferenceForDictionaryKey(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ return;
+ }
+
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData(info, context);
+ return ;
+ }
+
+ /// <summary>
+ /// This hash code is informed by (but not the same as) the target object, and is constant for the life
+ /// of the WeakReference.
+ /// </summary>
+ /// <returns>A hash code for the current Object.</returns>
+ public override int GetHashCode()
+ {
+ return hashCode;
+ }
+
+ /// <summary>
+ /// Equality is trickier than the Hashcode, because of the possibility that the target of either WeakReference
+ /// has been GC'ed or finalized. In those cases, when it is not known that the references must have refered to
+ /// the same object, Equals() returns false. Otherwise, it returns the same as the underlying Equals in the
+ /// target objects.
+ /// </summary>
+ /// <param name="obj">The Object to compare with the current Object</param>
+ /// <returns>true if the specified Object is equal to the current Object; otherwise false.</returns>
+ public override bool Equals(object obj)
+ {
+ bool result = false;
+ WeakReferenceForDictionaryKey other = obj as WeakReferenceForDictionaryKey;
+
+ if (Object.ReferenceEquals(other, null))
+ {
+ result = false;
+ }
+ else if (Object.ReferenceEquals(this, other))
+ {
+ result = true;
+ }
+ else
+ {
+ object thisObj = null;
+ object otherObj = null;
+
+ try
+ {
+ thisObj = this.Target;
+ otherObj = other.Target;
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ if (thisObj == null || otherObj == null)
+ {
+ // these objects either refered to null (is that even possible?) or refer to
+ // something that has been GC'ed/finalized. the latter case is the important
+ // scenario for us, and we'll return false, in effect causing each GC'ed
+ // weak reference to appear to be equal only to itself.
+ result = false;
+ }
+ else
+ {
+ result = Object.Equals(thisObj, otherObj);
+ }
+ }
+
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs b/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs
new file mode 100644
index 0000000..44130e0
--- /dev/null
+++ b/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs
@@ -0,0 +1,19 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The metadata interface for exporters and importers of metadata on <see cref="ITaggerProvider"/> factories.
+ /// </summary>
+ public interface INamedTaggerMetadata : ITaggerMetadata, INamedContentTypeMetadata
+ {
+ }
+}
diff --git a/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs b/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs
new file mode 100644
index 0000000..468144b
--- /dev/null
+++ b/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs
@@ -0,0 +1,23 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Tagging
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// The metadata interface for exporters and importers of metadata on <see cref="ITaggerProvider"/> factories.
+ /// </summary>
+ public interface ITaggerMetadata : IContentTypeMetadata
+ {
+ /// <summary>
+ /// The set of <see cref="TagTypeAttribute"/> objects.
+ /// </summary>
+ IEnumerable<Type> TagTypes { get; }
+ }
+}
diff --git a/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs b/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs
new file mode 100644
index 0000000..b9e7f60
--- /dev/null
+++ b/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs
@@ -0,0 +1,51 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Operations
+{
+ internal abstract class TextUndoPrimitive : ITextUndoPrimitive
+ {
+ private ITextUndoTransaction parent;
+
+ public virtual ITextUndoTransaction Parent
+ {
+ get { return this.parent; }
+ set { this.parent = value; }
+ }
+
+ public virtual bool CanRedo
+ {
+ get { return true; }
+ }
+
+ public virtual bool CanUndo
+ {
+ get { return true; }
+ }
+
+ protected virtual void Toggle()
+ {
+ }
+
+ public virtual void Do()
+ {
+ Toggle();
+ }
+
+ public virtual void Undo()
+ {
+ Toggle();
+ }
+
+ public virtual bool CanMerge(ITextUndoPrimitive older)
+ {
+ return false;
+ }
+
+ public virtual ITextUndoPrimitive Merge(ITextUndoPrimitive older)
+ {
+ throw new System.NotSupportedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs
new file mode 100644
index 0000000..b243fc7
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs
@@ -0,0 +1,15 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Metadata which includes Content Types and Text View Roles
+ /// </summary>
+ public interface IContentTypeAndTextViewRoleMetadata : IContentTypeMetadata, ITextViewRoleMetadata
+ {
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs
new file mode 100644
index 0000000..8150cfd
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+using System.ComponentModel;
+
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ /// <summary>
+ /// Metadata which includes Content Types and Text View Roles
+ /// </summary>
+ public interface IDeferrableContentTypeAndTextViewRoleMetadata : IContentTypeAndTextViewRoleMetadata
+ {
+ /// <summary>
+ /// Optional OptionId that controls creation of the extension.
+ /// </summary>
+ [DefaultValue(null)]
+ string OptionName { get; }
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs
new file mode 100644
index 0000000..4307b0c
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs
@@ -0,0 +1,13 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System.Collections.Generic;
+
+ public interface ITextViewRoleMetadata
+ {
+ IEnumerable<string> TextViewRoles { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs b/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs
new file mode 100644
index 0000000..2f81035
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System.ComponentModel;
+ using System.Windows;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Utilities;
+
+ public interface IWpfTextViewMarginMetadata : IOrderable, IContentTypeAndTextViewRoleMetadata
+ {
+ /// <summary>
+ /// Gets the name of the margin that contains this margin.
+ /// </summary>
+ string MarginContainer { get; }
+
+ [DefaultValue(null)]
+ IEnumerable<string> Replaces { get; }
+
+ /// <summary>
+ /// Optional OptionName that controls creation and visibility of the margin.
+ /// </summary>
+ [DefaultValue(null)]
+ string OptionName { get; }
+
+ /// <summary>
+ /// Gets the grid unit type to be used for drawing of this element in the container margin's grid.
+ /// </summary>
+ [DefaultValue(GridUnitType.Auto)]
+ GridUnitType GridUnitType { get; }
+
+ /// <summary>
+ /// Gets the size of the grid cell in which the margin should be placed.
+ /// </summary>
+ [DefaultValue(1.0)]
+ double GridCellLength { get; }
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/UIExtensionSelector.cs b/src/Text/Util/TextUIUtil/UIExtensionSelector.cs
new file mode 100644
index 0000000..3a1ba27
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/UIExtensionSelector.cs
@@ -0,0 +1,83 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Utilities;
+
+ /// <summary>
+ /// Helper class to perform ContentType and TextViewRole match against a set of extensions.
+ /// </summary>
+ internal static class UIExtensionSelector
+ {
+ /// <summary>
+ /// Given a list of extensions that provide text view roles, filter the list and return that
+ /// subset which matches at least one of the roles in the provided set of roles.
+ /// </summary>
+ public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView>
+ (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles,
+ ITextViewRoleSet viewRoles)
+ where TMetadataView : ITextViewRoleMetadata // text view role is required
+ {
+ var result = new List<Lazy<TProvider, TMetadataView>>();
+ foreach (var providerHandle in providerHandles)
+ {
+ IEnumerable<string> providerRoles = providerHandle.Metadata.TextViewRoles;
+ if (viewRoles.ContainsAny(providerRoles))
+ {
+ result.Add(providerHandle);
+ }
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Given a list of extensions that provide text view roles and content types, filter the list and return that
+ /// subset which matches the content types and at least one of the roles in the provided set of roles.
+ /// </summary>
+ public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView>
+ (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles,
+ IContentType documentContentType,
+ IContentType excludedContentType,
+ ITextViewRoleSet viewRoles)
+ where TMetadataView : IContentTypeAndTextViewRoleMetadata // both content type and text view role are required
+ {
+ var result = new List<Lazy<TProvider, TMetadataView>>();
+ foreach (var providerHandle in providerHandles)
+ {
+ // first, check content type match
+ if ((excludedContentType == null || !ExtensionSelector.ContentTypeMatch(excludedContentType, providerHandle.Metadata.ContentTypes)) &&
+ ExtensionSelector.ContentTypeMatch(documentContentType, providerHandle.Metadata.ContentTypes) &&
+ viewRoles.ContainsAny(providerHandle.Metadata.TextViewRoles))
+ {
+ result.Add(providerHandle);
+ }
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Given a list of extensions that provide text view roles and content types, return the
+ /// instantiated extension which best matches the given content type and matches at least one of the roles.
+ /// </summary>
+ public static TExtensionInstance InvokeBestMatchingFactory<TExtensionInstance, TExtensionFactory, TMetadataView>
+ (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> providerHandles,
+ IContentType dataContentType,
+ ITextViewRoleSet viewRoles,
+ Func<TExtensionFactory, TExtensionInstance> getter,
+ IContentTypeRegistryService contentTypeRegistryService,
+ GuardedOperations guardedOperations,
+ object errorSource)
+ where TMetadataView : IContentTypeAndTextViewRoleMetadata // both content type and text view role are required
+ where TExtensionFactory : class
+ where TExtensionInstance : class
+ {
+ var roleMatchingProviderHandles = SelectMatchingExtensions(providerHandles, viewRoles);
+ return guardedOperations.InvokeBestMatchingFactory(roleMatchingProviderHandles, dataContentType, getter, contentTypeRegistryService, errorSource);
+ }
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs
new file mode 100644
index 0000000..34fe9f3
--- /dev/null
+++ b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs
@@ -0,0 +1,77 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ using System;
+ using Microsoft.VisualStudio.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+
+ /// <summary>
+ /// Implements a text view model that simply reexposes the data buffer and an optional edit buffer. The VisualBuffer
+ /// is the same as the edit buffer, which is in turn the same as the data buffer if no edit buffer is specified.
+ /// This is the default if no view model provider is specified or if the specified one declines to build a model.
+ /// </summary>
+ internal class VacuousTextViewModel : ITextViewModel
+ {
+ private ITextDataModel dataModel;
+ private ITextBuffer editBuffer;
+
+ public VacuousTextViewModel(ITextDataModel dataModel) : this(dataModel, dataModel.DataBuffer) { }
+
+ public VacuousTextViewModel(ITextDataModel dataModel, ITextBuffer editBuffer)
+ {
+ this.dataModel = dataModel;
+ this.editBuffer = editBuffer;
+ this.Properties = new PropertyCollection();
+ }
+
+ public ITextDataModel DataModel
+ {
+ get { return this.dataModel; }
+ }
+
+ public ITextBuffer DataBuffer
+ {
+ get { return this.dataModel.DataBuffer; }
+ }
+
+ public ITextBuffer EditBuffer
+ {
+ get { return this.editBuffer; }
+ }
+
+ public ITextBuffer VisualBuffer
+ {
+ get { return this.EditBuffer; }
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ public PropertyCollection Properties { get; internal set; }
+
+
+ public SnapshotPoint GetNearestPointInVisualBuffer(SnapshotPoint editBufferPoint)
+ {
+ // The edit buffer is the same as the visual buffer, so just return the passed-in point.
+ return editBufferPoint;
+ }
+
+ public SnapshotPoint GetNearestPointInVisualSnapshot(SnapshotPoint editBufferPoint, ITextSnapshot targetVisualSnapshot, PointTrackingMode trackingMode)
+ {
+ // The edit buffer is the same as the visual buffer, so just return the passed-in point translated to the correct snapshot.
+ return editBufferPoint.TranslateTo(targetVisualSnapshot, trackingMode);
+ }
+
+ public bool IsPointInVisualBuffer(SnapshotPoint editBufferPoint, PositionAffinity affinity)
+ {
+ // The edit buffer is the same as the visual buffer, so just return true.
+ return true;
+ }
+ }
+}
diff --git a/src/key.snk b/src/key.snk
new file mode 100644
index 0000000..5ec4eb8
--- /dev/null
+++ b/src/key.snk
Binary files differ