1
1
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.
2
2
3
- using System . Diagnostics . CodeAnalysis ;
4
3
using Microsoft . CodeAnalysis ;
5
4
using Microsoft . CodeAnalysis . Rename ;
6
5
using Microsoft . VisualStudio . LanguageServices ;
7
6
using Microsoft . VisualStudio . OperationProgress ;
8
7
using Microsoft . VisualStudio . ProjectSystem . Waiting ;
9
8
using Microsoft . VisualStudio . Settings ;
9
+ using Microsoft . VisualStudio . Shell ;
10
+ using Microsoft . VisualStudio . Text ;
10
11
using Microsoft . VisualStudio . Threading ;
12
+ // Debug collides with Microsoft.VisualStudio.ProjectSystem.VS.Debug
13
+ using DiagDebug = System . Diagnostics . Debug ;
11
14
using Path = System . IO . Path ;
15
+ using static Microsoft . CodeAnalysis . Rename . Renamer ;
12
16
13
17
namespace Microsoft . VisualStudio . ProjectSystem . VS . Rename
14
18
{
@@ -21,15 +25,16 @@ internal class FileMoveNotificationListener : IFileMoveNotificationListener
21
25
22
26
private readonly UnconfiguredProject _unconfiguredProject ;
23
27
private readonly IUserNotificationServices _userNotificationServices ;
24
- private readonly IUnconfiguredProjectVsServices _projectVsServices ;
25
28
private readonly Workspace _workspace ;
26
29
private readonly IProjectThreadingService _threadingService ;
27
30
private readonly IVsService < SVsOperationProgress , IVsOperationProgressStatusService > _operationProgressService ;
28
31
private readonly IWaitIndicator _waitService ;
29
32
private readonly IRoslynServices _roslynServices ;
30
33
private readonly IVsService < SVsSettingsPersistenceManager , ISettingsManager > _settingsManagerService ;
31
34
32
- private List < ( Renamer . RenameDocumentActionSet Set , string FileName ) > ? _actions ;
35
+ // The file-paths are the full disk path of the source file (path prior to moving the item).
36
+ private readonly Dictionary < string , RenameDocumentActionSet > _renameActionSetByFilePath = new ( ) ;
37
+ private string ? _renameMessage ;
33
38
34
39
[ ImportingConstructor ]
35
40
public FileMoveNotificationListener (
@@ -45,7 +50,6 @@ public FileMoveNotificationListener(
45
50
{
46
51
_unconfiguredProject = unconfiguredProject ;
47
52
_userNotificationServices = userNotificationServices ;
48
- _projectVsServices = projectVsServices ;
49
53
_workspace = workspace ;
50
54
_threadingService = threadingService ;
51
55
_operationProgressService = operationProgressService ;
@@ -56,190 +60,146 @@ public FileMoveNotificationListener(
56
60
57
61
public async Task OnBeforeFilesMovedAsync ( IReadOnlyCollection < IFileMoveItem > items )
58
62
{
59
- Project ? project = GetCurrentProject ( ) ;
60
-
61
- if ( project is not null && TryGetFilesToMove ( out List < ( string file , string destination ) > ? filesToMove ) )
62
- {
63
- _actions = await GetNamespaceUpdateActionsAsync ( ) ;
64
- }
65
- else
66
- {
67
- _actions = null ;
68
- }
69
-
70
- return ;
71
-
72
- Project ? GetCurrentProject ( )
63
+ Project ? project = _workspace . CurrentSolution . Projects . FirstOrDefault ( p => StringComparers . Paths . Equals ( p . FilePath , _unconfiguredProject . FullPath ) ) ;
64
+ if ( project is null )
73
65
{
74
- return _workspace . CurrentSolution . Projects . FirstOrDefault (
75
- proj => StringComparers . Paths . Equals ( proj . FilePath , _projectVsServices . Project . FullPath ) ) ;
66
+ return ;
76
67
}
77
68
78
- bool TryGetFilesToMove ( [ NotNullWhen ( returnValue : true ) ] out List < ( string file , string destination ) > ? filesToMove )
69
+ foreach ( IFileMoveItem itemToMove in GetFilesToMove ( items ) )
79
70
{
80
- filesToMove = null ;
81
-
82
- foreach ( IFileMoveItem item in items )
71
+ Document ? currentDocument = project . Documents . FirstOrDefault ( d => StringComparers . Paths . Equals ( d . FilePath , itemToMove . Source ) ) ;
72
+ if ( currentDocument is null )
83
73
{
84
- RecursiveTryGetFilesToMove ( item , ref filesToMove ) ;
74
+ continue ;
85
75
}
86
76
87
- return filesToMove is not null ;
88
- }
77
+ // Get the relative folder path from the project to the destination.
78
+ string destinationFolderPath = Path . GetDirectoryName ( _unconfiguredProject . MakeRelative ( itemToMove . Destination ) ) ;
79
+ string [ ] destinationFolders = destinationFolderPath . Split ( Delimiter . Path , StringSplitOptions . RemoveEmptyEntries ) ;
89
80
90
- void RecursiveTryGetFilesToMove ( IFileMoveItem ? item , ref List < ( string file , string destination ) > ? filesToMove )
91
- {
92
- if ( item is null )
81
+ // Since this rename only moves the location of the file to another directory, it will use the SyncNamespaceDocumentAction in Roslyn as the rename action within this set.
82
+ // The logic for selecting this rename action can be found here: https://github.com/dotnet/roslyn/blob/960f375f4825a189937d4bfd9fea8162ecc63177/src/Workspaces/Core/Portable/Rename/Renamer.cs#L133-L136
83
+ RenameDocumentActionSet renameActionSet = await RenameDocumentAsync ( currentDocument , s_renameOptions , null , destinationFolders ) ;
84
+ if ( renameActionSet . ApplicableActions . IsEmpty || renameActionSet . ApplicableActions . Any ( aa => aa . GetErrors ( ) . Any ( ) ) )
93
85
{
94
- return ;
86
+ continue ;
95
87
}
96
88
97
- if ( item . IsFolder )
98
- {
99
- if ( item is not ICopyPasteItem copyPasteItem )
100
- {
101
- return ;
102
- }
89
+ // Getting the rename message requires an instance of RenameDocumentAction.
90
+ // We only need to set this message text once for the lifetime of the class, since it isn't dynamic.
91
+ // Even though it isn't dynamic, it does get localized appropriately in Roslyn.
92
+ // The text in English is "Sync namespace to folder structure".
93
+ _renameMessage ??= renameActionSet . ApplicableActions . First ( ) . GetDescription ( ) ;
103
94
104
- foreach ( IFileMoveItem child in copyPasteItem . Children . Cast < IFileMoveItem > ( ) )
105
- {
106
- RecursiveTryGetFilesToMove ( child , ref filesToMove ) ;
107
- }
108
- }
109
- else
110
- {
111
- bool isCompileItem = StringComparers . ItemTypes . Equals ( item . ItemType , Compile . SchemaName ) ;
112
-
113
- if ( item . WithinProject && isCompileItem && ! item . IsLinked && ! item . IsFolder )
114
- {
115
- filesToMove ??= new ( ) ;
116
- filesToMove . Add ( ( item . Source , item . Destination ) ) ;
117
- }
118
- }
95
+ // Add the full source file-path of the item as the key for the rename action set.
96
+ _renameActionSetByFilePath . Add ( itemToMove . Source , renameActionSet ) ;
119
97
}
120
98
121
- async Task < List < ( Renamer . RenameDocumentActionSet , string ) > > GetNamespaceUpdateActionsAsync ( )
122
- {
123
- List < ( Renamer . RenameDocumentActionSet , string ) > actions = new ( ) ;
99
+ return ;
124
100
125
- foreach ( ( string filenameWithPath , string destination ) in filesToMove )
101
+ static IEnumerable < IFileMoveItem > GetFilesToMove ( IEnumerable < IFileMoveItem > items )
102
+ {
103
+ var itemQueue = new Queue < IFileMoveItem > ( items ) ;
104
+ while ( itemQueue . Count > 0 )
126
105
{
127
- string destinationFileRelative = _unconfiguredProject . MakeRelative ( destination ) ;
128
- string destinationFolder = Path . GetDirectoryName ( destinationFileRelative ) ;
129
- string [ ] documentFolders = destinationFolder . Split ( Delimiter . Path , StringSplitOptions . RemoveEmptyEntries ) ;
130
-
131
- string filename = Path . GetFileName ( filenameWithPath ) ;
106
+ IFileMoveItem item = itemQueue . Dequeue ( ) ;
132
107
133
- Document ? oldDocument = project . Documents . FirstOrDefault ( d => StringComparers . Paths . Equals ( d . FilePath , filenameWithPath ) ) ;
134
-
135
- if ( oldDocument is null )
108
+ // Termination condition
109
+ if ( item is { WithinProject : true , IsFolder : false , IsLinked : false } &&
110
+ StringComparers . ItemTypes . Equals ( item . ItemType , Compile . SchemaName ) )
136
111
{
112
+ yield return item ;
137
113
continue ;
138
114
}
139
115
140
- // This is a file item to another directory, it should only detect this a Update Namespace action.
141
- Renamer . RenameDocumentActionSet documentAction = await Renamer . RenameDocumentAsync ( oldDocument , s_renameOptions , null , documentFolders ) ;
142
-
143
- if ( documentAction . ApplicableActions . IsEmpty ||
144
- documentAction . ApplicableActions . Any ( a => ! a . GetErrors ( ) . IsEmpty ) )
116
+ // Folder navigation
117
+ if ( item is { IsFolder : true } and ICopyPasteItem copyPasteItem )
145
118
{
146
- continue ;
119
+ IEnumerable < IFileMoveItem > children = copyPasteItem . Children . Select ( c => c as IFileMoveItem ) . WhereNotNull ( ) ;
120
+ foreach ( IFileMoveItem child in children )
121
+ {
122
+ itemQueue . Enqueue ( child ) ;
123
+ }
147
124
}
148
-
149
- actions . Add ( ( documentAction , filename ) ) ;
150
125
}
151
-
152
- return actions ;
153
126
}
154
127
}
155
128
156
129
public async Task OnAfterFileMoveAsync ( )
157
130
{
158
- if ( _actions is { Count : not 0 } && await CheckUserConfirmationAsync ( ) )
131
+ if ( ! _renameActionSetByFilePath . Any ( ) || ! await IsEnabledOrConfirmedAsync ( ) )
159
132
{
160
- ApplyNamespaceUpdateActions ( ) ;
133
+ // Clear the collection since the user declined (or has disabled) the rename namespace option.
134
+ _renameActionSetByFilePath . Clear ( ) ;
135
+ return ;
161
136
}
162
137
163
- return ;
164
-
165
- async Task < bool > CheckUserConfirmationAsync ( )
138
+ _ = _threadingService . JoinableTaskFactory . RunAsync ( async ( ) =>
166
139
{
167
- ISettingsManager settings = await _settingsManagerService . GetValueAsync ( ) ;
140
+ // The wait service requires the main thread to run.
141
+ await _threadingService . SwitchToUIThread ( ) ;
142
+ // Displays a dialog showing the progress of updating the namespaces in the files.
143
+ _waitService . Run (
144
+ title : string . Empty ,
145
+ message : _renameMessage ! ,
146
+ allowCancel : true ,
147
+ asyncMethod : ApplyRenamesAsync ,
148
+ totalSteps : _renameActionSetByFilePath . Count ) ;
149
+ } ) ;
168
150
169
- bool promptNamespaceUpdate = settings . GetValueOrDefault ( VsToolsOptions . OptionPromptNamespaceUpdate , true ) ;
170
- bool enabledNamespaceUpdate = settings . GetValueOrDefault ( VsToolsOptions . OptionEnableNamespaceUpdate , true ) ;
151
+ return ;
171
152
172
- if ( ! enabledNamespaceUpdate || ! promptNamespaceUpdate )
153
+ async Task ApplyRenamesAsync ( IWaitContext context )
154
+ {
155
+ CancellationToken token = context . CancellationToken ;
156
+ await TaskScheduler . Default ;
157
+ // WORKAROUND: We don't yet have a way to wait for the changes to propagate to Roslyn, tracked by https://github.com/dotnet/project-system/issues/3425
158
+ // Instead, we wait for the IntelliSense stage to finish for the entire solution.
159
+ IVsOperationProgressStatusService statusService = await _operationProgressService . GetValueAsync ( token ) ;
160
+ await statusService . GetStageStatus ( CommonOperationProgressStageIds . Intellisense ) . WaitForCompletionAsync ( ) . WithCancellation ( token ) ;
161
+ // After waiting, a "new" published Solution is available.
162
+ Solution solution = _workspace . CurrentSolution ;
163
+
164
+ int currentStep = 1 ;
165
+ foreach ( ( string filePath , RenameDocumentActionSet renameActionSet ) in _renameActionSetByFilePath )
173
166
{
174
- return enabledNamespaceUpdate ;
175
- }
176
-
177
- await _projectVsServices . ThreadingService . SwitchToUIThread ( ) ;
167
+ // Display the filename being updated to the user in the progress dialog.
168
+ context . Update ( currentStep : currentStep ++ , progressText : Path . GetFileName ( filePath ) ) ;
178
169
179
- bool confirmation = _userNotificationServices . Confirm ( VSResources . UpdateNamespacePromptMessage , out promptNamespaceUpdate ) ;
180
-
181
- await settings . SetValueAsync ( VsToolsOptions . OptionPromptNamespaceUpdate , ! promptNamespaceUpdate , true ) ;
182
-
183
- // If the user checked the "Don't show again" checkbox, we need to set the namespace enable state based on their selection of Yes/No in the dialog.
184
- if ( promptNamespaceUpdate )
185
- {
186
- await settings . SetValueAsync ( VsToolsOptions . OptionEnableNamespaceUpdate , confirmation , isMachineLocal : true ) ;
170
+ solution = await renameActionSet . UpdateSolutionAsync ( solution , token ) ;
187
171
}
188
172
189
- return confirmation ;
173
+ await _threadingService . SwitchToUIThread ( token ) ;
174
+ bool areChangesApplied = _roslynServices . ApplyChangesToSolution ( _workspace , solution ) ;
175
+ DiagDebug . Assert ( areChangesApplied , "ApplyChangesToSolution returned false" ) ;
176
+ // Clear the collection after it has been processed.
177
+ _renameActionSetByFilePath . Clear ( ) ;
190
178
}
191
179
192
- void ApplyNamespaceUpdateActions ( )
180
+ async Task < bool > IsEnabledOrConfirmedAsync ( )
193
181
{
194
- _ = _threadingService . JoinableTaskFactory . RunAsync ( async ( ) =>
195
- {
196
- await _projectVsServices . ThreadingService . SwitchToUIThread ( ) ;
197
-
198
- string message = _actions . First ( ) . Set . ApplicableActions . First ( ) . GetDescription ( ) ;
199
-
200
- _waitService . Run (
201
- title : "" ,
202
- message : message ,
203
- allowCancel : true ,
204
- async context =>
205
- {
206
- await TaskScheduler . Default ;
207
-
208
- Solution solution = await PublishLatestSolutionAsync ( context . CancellationToken ) ;
209
-
210
- int currentStep = 1 ;
211
-
212
- foreach ( ( Renamer . RenameDocumentActionSet action , string fileName ) in _actions )
213
- {
214
- context . Update ( currentStep : currentStep ++ , progressText : fileName ) ;
215
-
216
- solution = await action . UpdateSolutionAsync ( solution , context . CancellationToken ) ;
217
- }
218
-
219
- await _projectVsServices . ThreadingService . SwitchToUIThread ( ) ;
220
-
221
- bool applied = _roslynServices . ApplyChangesToSolution ( solution . Workspace , solution ) ;
222
-
223
- System . Diagnostics . Debug . Assert ( applied , "ApplyChangesToSolution returned false" ) ;
224
- } ,
225
- totalSteps : _actions . Count ) ;
226
- } ) ;
182
+ ISettingsManager settings = await _settingsManagerService . GetValueAsync ( ) ;
227
183
228
- async Task < Solution > PublishLatestSolutionAsync ( CancellationToken cancellationToken )
184
+ bool isEnabled = settings . GetValueOrDefault ( VsToolsOptions . OptionEnableNamespaceUpdate , defaultValue : true ) ;
185
+ bool isPromptEnabled = settings . GetValueOrDefault ( VsToolsOptions . OptionPromptNamespaceUpdate , defaultValue : true ) ;
186
+ // If not enabled, returns false.
187
+ // If enabled but prompt is not enabled, returns true.
188
+ // Otherwise, we display the prompt to the user.
189
+ if ( ! isEnabled || ! isPromptEnabled )
229
190
{
230
- // WORKAROUND: We don't yet have a way to wait for the changes to propagate
231
- // to Roslyn (tracked by https://github.com/dotnet/project-system/issues/3425), so
232
- // instead we wait for the IntelliSense stage to finish for the entire solution
233
-
234
- IVsOperationProgressStatusService operationProgressStatusService = await _operationProgressService . GetValueAsync ( cancellationToken ) ;
235
-
236
- IVsOperationProgressStageStatus stageStatus = operationProgressStatusService . GetStageStatus ( CommonOperationProgressStageIds . Intellisense ) ;
237
-
238
- await stageStatus . WaitForCompletionAsync ( ) . WithCancellation ( cancellationToken ) ;
191
+ return isEnabled ;
192
+ }
239
193
240
- // The result of that wait, is basically a "new" published Solution, so grab it
241
- return _workspace . CurrentSolution ;
194
+ await _threadingService . SwitchToUIThread ( ) ;
195
+ bool isConfirmed = _userNotificationServices . Confirm ( VSResources . UpdateNamespacePromptMessage , out bool disablePromptMessage ) ;
196
+ await settings . SetValueAsync ( VsToolsOptions . OptionPromptNamespaceUpdate , ! disablePromptMessage , isMachineLocal : true ) ;
197
+ // If the user checked the "Don't show again" checkbox, we need to set the namespace enable state based on their selection of Yes/No in the dialog.
198
+ if ( disablePromptMessage )
199
+ {
200
+ await settings . SetValueAsync ( VsToolsOptions . OptionEnableNamespaceUpdate , isConfirmed , isMachineLocal : true ) ;
242
201
}
202
+ return isConfirmed ;
243
203
}
244
204
}
245
205
}
0 commit comments