-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplify error handling and notifications in rename. #61515
Changes from 5 commits
645a140
06e9da9
a3599eb
d8d517b
fcf7da2
116376e
5b5a543
d6c09fb
1fd3092
752c9f2
f0ee857
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -788,24 +788,31 @@ private void CommitCore(IUIThreadOperationContext operationContext, bool preview | |
|
||
DismissUIAndRollbackEditsAndEndRenameSession( | ||
RenameLogMessage.UserActionOutcome.Committed, previewChanges, | ||
() => ApplyRename(newSolution, operationContext)); | ||
() => | ||
{ | ||
var error = TryApplyRename(newSolution); | ||
|
||
if (error is not null) | ||
{ | ||
operationContext.TakeOwnership(); | ||
var notificationService = _workspace.Services.GetService<INotificationService>(); | ||
notificationService.SendNotification( | ||
error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
private void ApplyRename(Solution newSolution, IUIThreadOperationContext operationContext) | ||
/// <summary> | ||
/// Returns non-null error message if renaming fails. | ||
/// </summary> | ||
private (NotificationSeverity severity, string message)? TryApplyRename(Solution newSolution) | ||
{ | ||
var changes = _baseSolution.GetChanges(newSolution); | ||
var changedDocumentIDs = changes.GetProjectChanges().SelectMany(c => c.GetChangedDocuments()).ToList(); | ||
|
||
if (!_renameInfo.TryOnBeforeGlobalSymbolRenamed(_workspace, changedDocumentIDs, this.ReplacementText)) | ||
{ | ||
var notificationService = _workspace.Services.GetService<INotificationService>(); | ||
notificationService.SendNotification( | ||
EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid, | ||
EditorFeaturesResources.Rename_Symbol, | ||
NotificationSeverity.Error); | ||
return; | ||
} | ||
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super unfortunate that this external-notification api we have doesn't have any way for the component returning 'false' to say what the issue is. So we have to give this unhelpful message here. |
||
|
||
using var undoTransaction = _workspace.OpenGlobalUndoTransaction(EditorFeaturesResources.Inline_Rename); | ||
var finalSolution = newSolution.Workspace.CurrentSolution; | ||
|
@@ -837,7 +844,10 @@ private void ApplyRename(Solution newSolution, IUIThreadOperationContext operati | |
finalSolution = finalSolution.WithDocumentName(id, newDocument.Name); | ||
} | ||
|
||
if (_workspace.TryApplyChanges(finalSolution)) | ||
if (!_workspace.TryApplyChanges(finalSolution)) | ||
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed from the silent failure approach from ebfore to at least having a notification about what happened. |
||
|
||
try | ||
{ | ||
// Since rename can apply file changes as well, and those file | ||
// changes can generate new document ids, include added documents | ||
|
@@ -846,20 +856,19 @@ private void ApplyRename(Solution newSolution, IUIThreadOperationContext operati | |
var finalChanges = _workspace.CurrentSolution.GetChanges(_baseSolution); | ||
|
||
var finalChangedIds = finalChanges | ||
.GetProjectChanges() | ||
.SelectMany(c => c.GetChangedDocuments().Concat(c.GetAddedDocuments())) | ||
.ToList(); | ||
.GetProjectChanges() | ||
.SelectMany(c => c.GetChangedDocuments().Concat(c.GetAddedDocuments())) | ||
.ToList(); | ||
|
||
if (!_renameInfo.TryOnAfterGlobalSymbolRenamed(_workspace, finalChangedIds, this.ReplacementText)) | ||
{ | ||
var notificationService = _workspace.Services.GetService<INotificationService>(); | ||
operationContext.TakeOwnership(); | ||
notificationService.SendNotification( | ||
EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated, | ||
EditorFeaturesResources.Rename_Symbol, | ||
NotificationSeverity.Information); | ||
} | ||
return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kept this informational. but it certainly seems weird that we're effectivley saying "hey... heads up... things might be bad, but we have no idea why or waht you can do about it" :( |
||
|
||
return null; | ||
} | ||
finally | ||
{ | ||
// If we successfully update the workspace then make sure the undo transaction is committed and is | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// always able to undo anything any other external listener did. | ||
undoTransaction.Commit(); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,16 +49,24 @@ public RenameTrackingCommitter( | |
_renameSymbolResultGetter = new AsyncLazy<RenameTrackingSolutionSet>(c => RenameSymbolWorkerAsync(c), cacheResult: true); | ||
} | ||
|
||
public void Commit(CancellationToken cancellationToken) | ||
/// <summary> | ||
/// Returns non-null error message if renaming fails. | ||
/// </summary> | ||
public (NotificationSeverity severity, string message)? TryCommit(CancellationToken cancellationToken) | ||
{ | ||
_stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); | ||
|
||
ApplyChangesToWorkspace(cancellationToken); | ||
|
||
// Clear the state machine so that future updates to the same token work, | ||
// and any text changes caused by this update are not interpreted as | ||
// potential renames | ||
_stateMachine.ClearTrackingSession(); | ||
try | ||
{ | ||
return TryApplyChangesToWorkspace(cancellationToken); | ||
} | ||
finally | ||
{ | ||
// Clear the state machine so that future updates to the same token work, | ||
// and any text changes caused by this update are not interpreted as | ||
// potential renames | ||
_stateMachine.ClearTrackingSession(); | ||
} | ||
} | ||
|
||
public async Task<RenameTrackingSolutionSet> RenameSymbolAsync(CancellationToken cancellationToken) | ||
|
@@ -82,7 +90,10 @@ private async Task<RenameTrackingSolutionSet> RenameSymbolWorkerAsync(Cancellati | |
return new RenameTrackingSolutionSet(symbol, solutionWithOriginalName, renamedSolution); | ||
} | ||
|
||
private void ApplyChangesToWorkspace(CancellationToken cancellationToken) | ||
/// <summary> | ||
/// Returns non-null error message if renaming fails. | ||
/// </summary> | ||
private (NotificationSeverity, string)? TryApplyChangesToWorkspace(CancellationToken cancellationToken) | ||
{ | ||
_stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); | ||
|
||
|
@@ -120,48 +131,46 @@ private void ApplyChangesToWorkspace(CancellationToken cancellationToken) | |
// update the state machine after undo/redo actions. | ||
|
||
var changedDocuments = renameTrackingSolutionSet.RenamedSolution.GetChangedDocuments(renameTrackingSolutionSet.OriginalSolution); | ||
|
||
// When this action is undone (the user has undone twice), restore the state | ||
// machine to so that they can continue their original rename tracking session. | ||
|
||
var trackingSessionId = _stateMachine.StoreCurrentTrackingSessionAndGenerateId(); | ||
UpdateWorkspaceForResetOfTypedIdentifier(workspace, renameTrackingSolutionSet.OriginalSolution, trackingSessionId); | ||
|
||
// Now that the solution is back in its original state, notify third parties about | ||
// the coming rename operation. | ||
if (!_refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, renameTrackingSolutionSet.Symbol, newName, throwOnFailure: false)) | ||
try | ||
{ | ||
var notificationService = workspace.Services.GetService<INotificationService>(); | ||
notificationService.SendNotification( | ||
EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid, | ||
EditorFeaturesResources.Rename_Symbol, | ||
NotificationSeverity.Error); | ||
|
||
return; | ||
// When this action is undone (the user has undone twice), restore the state | ||
// machine to so that they can continue their original rename tracking session. | ||
|
||
var trackingSessionId = _stateMachine.StoreCurrentTrackingSessionAndGenerateId(); | ||
var result = TryUpdateWorkspaceForResetOfTypedIdentifier(workspace, renameTrackingSolutionSet.OriginalSolution, trackingSessionId); | ||
if (result is not null) | ||
return result; | ||
|
||
// Now that the solution is back in its original state, notify third parties about | ||
// the coming rename operation. | ||
if (!_refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, renameTrackingSolutionSet.Symbol, newName, throwOnFailure: false)) | ||
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid); | ||
|
||
// move all changes to final solution based on the workspace's current solution, since the current solution | ||
// got updated when we reset it above. | ||
var finalSolution = workspace.CurrentSolution; | ||
foreach (var docId in changedDocuments) | ||
{ | ||
// because changes have already been made to the workspace (UpdateWorkspaceForResetOfTypedIdentifier() above), | ||
// these calls can't be cancelled and must be allowed to complete. | ||
var root = renameTrackingSolutionSet.RenamedSolution.GetDocument(docId).GetSyntaxRootSynchronously(CancellationToken.None); | ||
finalSolution = finalSolution.WithDocumentSyntaxRoot(docId, root); | ||
} | ||
|
||
// Undo/redo on this action must always clear the state machine | ||
return TryUpdateWorkspaceForGlobalIdentifierRename( | ||
workspace, | ||
finalSolution, | ||
_displayText, | ||
changedDocuments, | ||
renameTrackingSolutionSet.Symbol, | ||
newName, | ||
trackingSessionId); | ||
} | ||
|
||
// move all changes to final solution based on the workspace's current solution, since the current solution | ||
// got updated when we reset it above. | ||
var finalSolution = workspace.CurrentSolution; | ||
foreach (var docId in changedDocuments) | ||
finally | ||
{ | ||
// because changes have already been made to the workspace (UpdateWorkspaceForResetOfTypedIdentifier() above), | ||
// these calls can't be cancelled and must be allowed to complete. | ||
var root = renameTrackingSolutionSet.RenamedSolution.GetDocument(docId).GetSyntaxRootSynchronously(CancellationToken.None); | ||
finalSolution = finalSolution.WithDocumentSyntaxRoot(docId, root); | ||
RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fairly certain not being in a finally here was a bug. the return at https://github.com/dotnet/roslyn/pull/61515/files?diff=unified&w=1#diff-444ebec94ff45aa833894aec3cf66ed4c3f13f5f4fc661c5b5ce17d730597d48L140 meant we could bail out without dismissing our rename tracking state. |
||
|
||
// Undo/redo on this action must always clear the state machine | ||
UpdateWorkspaceForGlobalIdentifierRename( | ||
workspace, | ||
finalSolution, | ||
_displayText, | ||
changedDocuments, | ||
renameTrackingSolutionSet.Symbol, | ||
newName, | ||
trackingSessionId); | ||
|
||
RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments); | ||
} | ||
|
||
private Solution CreateSolutionWithOriginalName(Document document, CancellationToken cancellationToken) | ||
|
@@ -203,7 +212,10 @@ private async Task<ISymbol> TryGetSymbolAsync(Solution solutionWithOriginalName, | |
return tokenRenameInfo.HasSymbols ? tokenRenameInfo.Symbols.First() : null; | ||
} | ||
|
||
private void UpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solution newSolution, int trackingSessionId) | ||
/// <summary> | ||
/// Returns non-null error message if renaming fails. | ||
/// </summary> | ||
private (NotificationSeverity, string)? TryUpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solution newSolution, int trackingSessionId) | ||
{ | ||
_stateMachine.ThreadingContext.ThrowIfNotOnUIThread(); | ||
|
||
|
@@ -217,18 +229,24 @@ private void UpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solut | |
localUndoTransaction.AddUndo(undoPrimitiveBefore); | ||
|
||
if (!workspace.TryApplyChanges(newSolution)) | ||
{ | ||
Contract.Fail("Rename Tracking could not update solution."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fairly certain this would just corrupt state in the rename engine (due to the lack of |
||
} | ||
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); | ||
|
||
// If we successfully update the workspace then make sure the undo transaction is committed and is | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// always able to undo anything any other external listener did. | ||
|
||
// Never resume tracking session on redo | ||
var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); | ||
localUndoTransaction.AddUndo(undoPrimitiveAfter); | ||
|
||
localUndoTransaction.Complete(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure waht the best logic is to have here. previously, if applying failed, we would throw in Contract.Fail. so this code woudl never run. i'm preserving that logic here... which i think is desirable. if we didn't change the workspace we don't need an undo added to the stack. but i'm not sure. we coudl always add these no matter what, but i'm not sure the implication of that. we will at least add these on success, which was our logic from before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
return null; | ||
} | ||
|
||
private void UpdateWorkspaceForGlobalIdentifierRename( | ||
/// <summary> | ||
/// Returns non-null error message if renaming fails. | ||
/// </summary> | ||
private (NotificationSeverity, string)? TryUpdateWorkspaceForGlobalIdentifierRename( | ||
Workspace workspace, | ||
Solution newSolution, | ||
string undoName, | ||
|
@@ -251,25 +269,27 @@ private void UpdateWorkspaceForGlobalIdentifierRename( | |
localUndoTransaction.AddUndo(undoPrimitiveBefore); | ||
|
||
if (!workspace.TryApplyChanges(newSolution)) | ||
{ | ||
Contract.Fail("Rename Tracking could not update solution."); | ||
} | ||
return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace); | ||
|
||
if (!_refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: false)) | ||
try | ||
{ | ||
var notificationService = workspace.Services.GetService<INotificationService>(); | ||
notificationService.SendNotification( | ||
EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated, | ||
EditorFeaturesResources.Rename_Symbol, | ||
NotificationSeverity.Information); | ||
if (!_refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: false)) | ||
return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the difference in experience if an error vs information is returned? Just thinking if something fails wouldn't that always be an error, or is failure here expected? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it affects the glyph we show in the notification window. i agree on this being wonky, but i decided to preserve semantics here vs change things... |
||
|
||
return null; | ||
} | ||
finally | ||
{ | ||
// If we successfully update the workspace then make sure the undo transaction is committed and is | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// always able to undo anything any other external listener did. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same concept as before. if the workspace application fails, we don't add these. but if it succeeds, we always add this, regardless of waht our external-notification system does. |
||
|
||
// Never resume tracking session on redo | ||
var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); | ||
localUndoTransaction.AddUndo(undoPrimitiveAfter); | ||
// Never resume tracking session on redo | ||
var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false); | ||
localUndoTransaction.AddUndo(undoPrimitiveAfter); | ||
|
||
localUndoTransaction.Complete(); | ||
workspaceUndoTransaction.Commit(); | ||
localUndoTransaction.Complete(); | ||
workspaceUndoTransaction.Commit(); | ||
} | ||
} | ||
} | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
single place in inline-rename, and single place in rename-code-action where we notify the user of an error we couldnt' handle.