Skip to content

Commit 35efce2

Browse files
authored
Merge pull request #1779 from Hosch250/Issue545
Notify when source files are modified externally
2 parents 2e77382 + bb528bd commit 35efce2

File tree

10 files changed

+272
-22
lines changed

10 files changed

+272
-22
lines changed

RetailCoder.VBE/App.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public sealed class App : IDisposable
3434
private IRubberduckHooks _hooks;
3535
private bool _handleSinkEvents = true;
3636
private readonly BranchesViewViewModel _branchesVM;
37-
private readonly SourceControlViewViewModel _panelVM;
37+
private readonly SourceControlViewViewModel _sourceControlPanelVM;
3838

3939
private readonly Logger _logger;
4040

@@ -68,11 +68,11 @@ public App(VBE vbe, IMessageBox messageBox,
6868
_logger = LogManager.GetCurrentClassLogger();
6969

7070
var sourceControlPanel = (SourceControlPanel) sourceControlPresenter.Window();
71-
_panelVM = (SourceControlViewViewModel) sourceControlPanel.ViewModel;
72-
_branchesVM = (BranchesViewViewModel) _panelVM.TabItems.Single(t => t.ViewModel.Tab == SourceControlTab.Branches).ViewModel;
71+
_sourceControlPanelVM = (SourceControlViewViewModel) sourceControlPanel.ViewModel;
72+
_branchesVM = (BranchesViewViewModel) _sourceControlPanelVM.TabItems.Single(t => t.ViewModel.Tab == SourceControlTab.Branches).ViewModel;
7373

74-
_panelVM.OpenRepoStarted += DisableSinkEventHandlers;
75-
_panelVM.OpenRepoCompleted += EnableSinkEventHandlersAndUpdateCache;
74+
_sourceControlPanelVM.OpenRepoStarted += DisableSinkEventHandlers;
75+
_sourceControlPanelVM.OpenRepoCompleted += EnableSinkEventHandlersAndUpdateCache;
7676

7777
_branchesVM.LoadingComponentsStarted += DisableSinkEventHandlers;
7878
_branchesVM.LoadingComponentsCompleted += EnableSinkEventHandlersAndUpdateCache;
@@ -311,6 +311,8 @@ async void sink_ComponentRenamed(object sender, DispatcherRenamedEventArgs<VBCom
311311
return;
312312
}
313313

314+
_sourceControlPanelVM.HandleRenamedComponent(e.Item, e.OldName);
315+
314316
_logger.Debug("Component '{0}' was renamed to '{1}'.", e.OldName, e.Item.Name);
315317

316318
_parser.State.RemoveRenamedComponent(e.Item, e.OldName);
@@ -325,7 +327,7 @@ async void sink_ComponentRemoved(object sender, DispatcherEventArgs<VBComponent>
325327
return;
326328
}
327329

328-
_panelVM.RemoveComponent(e.Item);
330+
_sourceControlPanelVM.HandleRemovedComponent(e.Item);
329331

330332
_logger.Debug("Component '{0}' was removed.", e.Item.Name);
331333
_parser.State.ClearStateCache(e.Item, true);
@@ -353,7 +355,7 @@ async void sink_ComponentAdded(object sender, DispatcherEventArgs<VBComponent> e
353355
return;
354356
}
355357

356-
_panelVM.AddComponent(e.Item);
358+
_sourceControlPanelVM.HandleAddedComponent(e.Item);
357359

358360
_logger.Debug("Component '{0}' was added.", e.Item.Name);
359361
_parser.State.OnParseRequested(sender, e.Item);
@@ -454,10 +456,10 @@ public void Dispose()
454456
return;
455457
}
456458

457-
if (_panelVM != null)
459+
if (_sourceControlPanelVM != null)
458460
{
459-
_panelVM.OpenRepoStarted -= DisableSinkEventHandlers;
460-
_panelVM.OpenRepoCompleted -= EnableSinkEventHandlersAndUpdateCache;
461+
_sourceControlPanelVM.OpenRepoStarted -= DisableSinkEventHandlers;
462+
_sourceControlPanelVM.OpenRepoCompleted -= EnableSinkEventHandlersAndUpdateCache;
461463
}
462464

463465
if (_branchesVM != null)

RetailCoder.VBE/UI/RubberduckUI.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

RetailCoder.VBE/UI/RubberduckUI.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,4 +1629,7 @@ All our stargazers, likers &amp; followers, for the warm fuzzies
16291629
<data name="SourceControl_CreateNewRemoteRepo_NoOpenRepo" xml:space="preserve">
16301630
<value>No open repository to push to specified remote location.</value>
16311631
</data>
1632+
<data name="SourceControl_ExternalModifications" xml:space="preserve">
1633+
<value>A source file was modified out of the editor; should Rubberduck reload it?</value>
1634+
</data>
16321635
</root>

RetailCoder.VBE/UI/SourceControl/ChangesViewViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ private void Commit()
186186
{
187187
RaiseErrorEvent(ex.Message, ex.InnerException.Message, NotificationType.Error);
188188
}
189+
190+
CommitMessage = string.Empty;
189191
}
190192

191193
private void IncludeChanges(IFileStatusEntry fileStatusEntry)

RetailCoder.VBE/UI/SourceControl/SourceControlViewViewModel.cs

Lines changed: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
using System.Collections.ObjectModel;
44
using System.Drawing;
55
using System.Globalization;
6+
using System.IO;
67
using System.Linq;
78
using System.Windows.Forms;
89
using System.Windows.Input;
910
using System.Windows.Media.Imaging;
1011
using Microsoft.Vbe.Interop;
1112
using Ninject;
13+
using NLog;
1214
using Rubberduck.Parsing.VBA;
1315
using Rubberduck.SourceControl;
1416
using Rubberduck.UI.Command;
@@ -35,6 +37,10 @@ public sealed class SourceControlViewViewModel : ViewModelBase, IDisposable
3537
private readonly ISourceControlConfigProvider _configService;
3638
private readonly SourceControlSettings _config;
3739
private readonly ICodePaneWrapperFactory _wrapperFactory;
40+
private readonly IMessageBox _messageBox;
41+
private readonly FileSystemWatcher _fileSystemWatcher;
42+
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
43+
private static readonly IEnumerable<string> VbFileExtensions = new[] { "cls", "bas", "frm" };
3844

3945
public SourceControlViewViewModel(
4046
VBE vbe,
@@ -46,7 +52,8 @@ public SourceControlViewViewModel(
4652
[Named("branchesView")] IControlView branchesView,
4753
[Named("unsyncedCommitsView")] IControlView unsyncedCommitsView,
4854
[Named("settingsView")] IControlView settingsView,
49-
ICodePaneWrapperFactory wrapperFactory)
55+
ICodePaneWrapperFactory wrapperFactory,
56+
IMessageBox messageBox)
5057
{
5158
_vbe = vbe;
5259
_state = state;
@@ -58,6 +65,7 @@ public SourceControlViewViewModel(
5865
_configService = configService;
5966
_config = _configService.Create();
6067
_wrapperFactory = wrapperFactory;
68+
_messageBox = messageBox;
6169

6270
_initRepoCommand = new DelegateCommand(_ => InitRepo());
6371
_openRepoCommand = new DelegateCommand(_ => OpenRepo());
@@ -87,16 +95,18 @@ public SourceControlViewViewModel(
8795
Status = RubberduckUI.Offline;
8896

8997
ListenForErrors();
98+
99+
_fileSystemWatcher = new FileSystemWatcher();
90100
}
91101

92102
public void SetTab(SourceControlTab tab)
93103
{
94104
SelectedItem = TabItems.First(t => t.ViewModel.Tab == tab);
95105
}
96106

97-
public void AddComponent(VBComponent component)
107+
public void HandleAddedComponent(VBComponent component)
98108
{
99-
if (Provider == null) { return; }
109+
if (Provider == null || !Provider.HandleVbeSinkEvents) { return; }
100110

101111
var fileStatus = Provider.Status().SingleOrDefault(stat => stat.FilePath.Split('.')[0] == component.Name);
102112
if (fileStatus != null)
@@ -105,9 +115,9 @@ public void AddComponent(VBComponent component)
105115
}
106116
}
107117

108-
public void RemoveComponent(VBComponent component)
118+
public void HandleRemovedComponent(VBComponent component)
109119
{
110-
if (Provider == null) { return; }
120+
if (Provider == null || !Provider.HandleVbeSinkEvents) { return; }
111121

112122
var fileStatus = Provider.Status().SingleOrDefault(stat => stat.FilePath.Split('.')[0] == component.Name);
113123
if (fileStatus != null)
@@ -116,6 +126,27 @@ public void RemoveComponent(VBComponent component)
116126
}
117127
}
118128

129+
public void HandleRenamedComponent(VBComponent component, string oldName)
130+
{
131+
if (Provider == null || !Provider.HandleVbeSinkEvents) { return; }
132+
133+
var fileStatus = Provider.LastKnownStatus().SingleOrDefault(stat => stat.FilePath.Split('.')[0] == oldName);
134+
if (fileStatus != null)
135+
{
136+
var directory = Provider.CurrentRepository.LocalLocation;
137+
directory += directory.EndsWith("\\") ? string.Empty : "\\";
138+
139+
var fileExt = "." + fileStatus.FilePath.Split('.').Last();
140+
141+
_fileSystemWatcher.EnableRaisingEvents = false;
142+
File.Move(directory + fileStatus.FilePath, directory + component.Name + fileExt);
143+
_fileSystemWatcher.EnableRaisingEvents = true;
144+
145+
Provider.RemoveFile(oldName + fileExt, false);
146+
Provider.AddFile(component.Name + fileExt);
147+
}
148+
}
149+
119150
private static readonly IDictionary<NotificationType, BitmapImage> IconMappings =
120151
new Dictionary<NotificationType, BitmapImage>
121152
{
@@ -125,7 +156,7 @@ public void RemoveComponent(VBComponent component)
125156

126157
private void _state_StateChanged(object sender, ParserStateEventArgs e)
127158
{
128-
if (e.State == ParserState.Parsed)
159+
if (e.State == ParserState.Pending)
129160
{
130161
UiDispatcher.InvokeAsync(Refresh);
131162
}
@@ -139,6 +170,107 @@ public ISourceControlProvider Provider
139170
{
140171
_provider = value;
141172
SetChildPresenterSourceControlProviders(_provider);
173+
174+
if (_fileSystemWatcher.Path != LocalDirectory && Directory.Exists(_provider.CurrentRepository.LocalLocation))
175+
{
176+
_fileSystemWatcher.Path = _provider.CurrentRepository.LocalLocation;
177+
_fileSystemWatcher.EnableRaisingEvents = true;
178+
_fileSystemWatcher.IncludeSubdirectories = true;
179+
180+
_fileSystemWatcher.Created += _fileSystemWatcher_Created;
181+
_fileSystemWatcher.Deleted += _fileSystemWatcher_Deleted;
182+
_fileSystemWatcher.Renamed += _fileSystemWatcher_Renamed;
183+
_fileSystemWatcher.Changed += _fileSystemWatcher_Changed;
184+
}
185+
}
186+
}
187+
188+
private void _fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
189+
{
190+
// the file system filter doesn't support multiple filters
191+
if (!VbFileExtensions.Contains(e.Name.Split('.').Last()))
192+
{
193+
return;
194+
}
195+
196+
if (!Provider.NotifyExternalFileChanges)
197+
{
198+
return;
199+
}
200+
201+
Logger.Trace("File system watcher detected file changed");
202+
if (_messageBox.Show(RubberduckUI.SourceControl_ExternalModifications, RubberduckUI.SourceControlPanel_Caption,
203+
MessageBoxButtons.OKCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1) == DialogResult.OK)
204+
{
205+
Provider.ReloadComponent(e.Name);
206+
UiDispatcher.InvokeAsync(Refresh);
207+
}
208+
}
209+
210+
private void _fileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
211+
{
212+
// the file system filter doesn't support multiple filters
213+
if (!VbFileExtensions.Contains(e.Name.Split('.').Last()))
214+
{
215+
return;
216+
}
217+
218+
if (!Provider.NotifyExternalFileChanges)
219+
{
220+
return;
221+
}
222+
223+
Logger.Trace("File system watcher detected file renamed");
224+
if (_messageBox.Show(RubberduckUI.SourceControl_ExternalModifications, RubberduckUI.SourceControlPanel_Caption,
225+
MessageBoxButtons.OKCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1) == DialogResult.OK)
226+
{
227+
Provider.RemoveFile(e.OldFullPath, true);
228+
Provider.AddFile(e.FullPath);
229+
UiDispatcher.InvokeAsync(Refresh);
230+
}
231+
}
232+
233+
private void _fileSystemWatcher_Deleted(object sender, FileSystemEventArgs e)
234+
{
235+
// the file system filter doesn't support multiple filters
236+
if (!VbFileExtensions.Contains(e.Name.Split('.').Last()))
237+
{
238+
return;
239+
}
240+
241+
if (!Provider.NotifyExternalFileChanges)
242+
{
243+
return;
244+
}
245+
246+
Logger.Trace("File system watcher detected file deleted");
247+
if (_messageBox.Show(RubberduckUI.SourceControl_ExternalModifications, RubberduckUI.SourceControlPanel_Caption,
248+
MessageBoxButtons.OKCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1) == DialogResult.OK)
249+
{
250+
Provider.RemoveFile(e.FullPath, true);
251+
UiDispatcher.InvokeAsync(Refresh);
252+
}
253+
}
254+
255+
private void _fileSystemWatcher_Created(object sender, FileSystemEventArgs e)
256+
{
257+
// the file system filter doesn't support multiple filters
258+
if (!VbFileExtensions.Contains(e.Name.Split('.').Last()))
259+
{
260+
return;
261+
}
262+
263+
if (!Provider.NotifyExternalFileChanges)
264+
{
265+
return;
266+
}
267+
268+
Logger.Trace("File system watcher detected file created");
269+
if (_messageBox.Show(RubberduckUI.SourceControl_ExternalModifications, RubberduckUI.SourceControlPanel_Caption,
270+
MessageBoxButtons.OKCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1) == DialogResult.OK)
271+
{
272+
Provider.AddFile(e.FullPath);
273+
UiDispatcher.InvokeAsync(Refresh);
142274
}
143275
}
144276

@@ -653,6 +785,7 @@ private void OpenRepoAssignedToProject()
653785

654786
private void Refresh()
655787
{
788+
_fileSystemWatcher.EnableRaisingEvents = false;
656789
if (Provider == null)
657790
{
658791
OpenRepoAssignedToProject();
@@ -664,6 +797,11 @@ private void Refresh()
664797
tab.ViewModel.RefreshView();
665798
}
666799
}
800+
801+
if (Provider != null && Directory.Exists(Provider.CurrentRepository.LocalLocation))
802+
{
803+
_fileSystemWatcher.EnableRaisingEvents = true;
804+
}
667805
}
668806

669807
private bool ValidRepoExists()
@@ -824,6 +962,15 @@ public void Dispose()
824962
{
825963
_state.StateChanged -= _state_StateChanged;
826964
}
965+
966+
if (_fileSystemWatcher != null)
967+
{
968+
_fileSystemWatcher.Created -= _fileSystemWatcher_Created;
969+
_fileSystemWatcher.Deleted -= _fileSystemWatcher_Deleted;
970+
_fileSystemWatcher.Renamed -= _fileSystemWatcher_Renamed;
971+
_fileSystemWatcher.Changed -= _fileSystemWatcher_Changed;
972+
_fileSystemWatcher.Dispose();
973+
}
827974
}
828975
}
829976
}

0 commit comments

Comments
 (0)