/ src / modules / cmdpal / ext / Microsoft.CmdPal.Ext.Bookmark / BookmarksManager.cs
BookmarksManager.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.Linq;
  6  using System.Threading;
  7  using System.Threading.Tasks;
  8  using ManagedCommon;
  9  using Microsoft.CmdPal.Core.Common.Helpers;
 10  using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
 11  
 12  namespace Microsoft.CmdPal.Ext.Bookmarks;
 13  
 14  internal sealed partial class BookmarksManager : IDisposable, IBookmarksManager
 15  {
 16      private readonly IBookmarkDataSource _dataSource;
 17      private readonly BookmarkJsonParser _parser = new();
 18      private readonly SupersedingAsyncGate _savingGate;
 19      private readonly Lock _lock = new();
 20      private BookmarksData _bookmarksData = new();
 21  
 22      public event Action<BookmarkData>? BookmarkAdded;
 23  
 24      public event Action<BookmarkData, BookmarkData>? BookmarkUpdated; // old, new
 25  
 26      public event Action<BookmarkData>? BookmarkRemoved;
 27  
 28      public IReadOnlyCollection<BookmarkData> Bookmarks
 29      {
 30          get
 31          {
 32              lock (_lock)
 33              {
 34                  return _bookmarksData.Data.ToList().AsReadOnly();
 35              }
 36          }
 37      }
 38  
 39      public BookmarksManager(IBookmarkDataSource dataSource)
 40      {
 41          ArgumentNullException.ThrowIfNull(dataSource);
 42          _dataSource = dataSource;
 43          _savingGate = new SupersedingAsyncGate(WriteData);
 44          LoadBookmarksFromFile();
 45      }
 46  
 47      public BookmarkData Add(string name, string bookmark)
 48      {
 49          var newBookmark = new BookmarkData(name, bookmark);
 50  
 51          lock (_lock)
 52          {
 53              _bookmarksData.Data.Add(newBookmark);
 54              _ = SaveChangesAsync();
 55              BookmarkAdded?.Invoke(newBookmark);
 56              return newBookmark;
 57          }
 58      }
 59  
 60      public bool Remove(Guid id)
 61      {
 62          lock (_lock)
 63          {
 64              var bookmark = _bookmarksData.Data.FirstOrDefault(b => b.Id == id);
 65              if (bookmark != null && _bookmarksData.Data.Remove(bookmark))
 66              {
 67                  _ = SaveChangesAsync();
 68                  BookmarkRemoved?.Invoke(bookmark);
 69                  return true;
 70              }
 71  
 72              return false;
 73          }
 74      }
 75  
 76      public BookmarkData? Update(Guid id, string name, string bookmark)
 77      {
 78          lock (_lock)
 79          {
 80              var existingBookmark = _bookmarksData.Data.FirstOrDefault(b => b.Id == id);
 81              if (existingBookmark != null)
 82              {
 83                  var updatedBookmark = existingBookmark with
 84                  {
 85                      Name = name,
 86                      Bookmark = bookmark,
 87                  };
 88  
 89                  var index = _bookmarksData.Data.IndexOf(existingBookmark);
 90                  _bookmarksData.Data[index] = updatedBookmark;
 91  
 92                  _ = SaveChangesAsync();
 93                  BookmarkUpdated?.Invoke(existingBookmark, updatedBookmark);
 94                  return updatedBookmark;
 95              }
 96  
 97              return null;
 98          }
 99      }
100  
101      private void LoadBookmarksFromFile()
102      {
103          try
104          {
105              var jsonData = _dataSource.GetBookmarkData();
106              var bookmarksData = _parser.ParseBookmarks(jsonData);
107  
108              // Upgrade old bookmarks if necessary
109              // Pre .95 versions did not assign IDs to bookmarks
110              var upgraded = false;
111              for (var index = 0; index < bookmarksData.Data.Count; index++)
112              {
113                  var bookmark = bookmarksData.Data[index];
114                  if (bookmark.Id == Guid.Empty)
115                  {
116                      bookmarksData.Data[index] = bookmark with { Id = Guid.NewGuid() };
117                      upgraded = true;
118                  }
119              }
120  
121              lock (_lock)
122              {
123                  _bookmarksData = bookmarksData;
124              }
125  
126              // LOAD BEARING: Save upgraded data back to file
127              // This ensures that old bookmarks are not repeatedly upgraded on each load,
128              // as the hotkeys and aliases are tied to the generated bookmark IDs.
129              if (upgraded)
130              {
131                  _ = SaveChangesAsync();
132              }
133          }
134          catch (Exception ex)
135          {
136              Logger.LogError(ex.Message);
137          }
138      }
139  
140      private Task WriteData(CancellationToken arg)
141      {
142          List<BookmarkData> dataToSave;
143          lock (_lock)
144          {
145              dataToSave = _bookmarksData.Data.ToList();
146          }
147  
148          try
149          {
150              var jsonData = _parser.SerializeBookmarks(new BookmarksData { Data = dataToSave });
151              _dataSource.SaveBookmarkData(jsonData);
152          }
153          catch (Exception ex)
154          {
155              Logger.LogError($"Failed to save bookmarks: {ex.Message}");
156          }
157  
158          return Task.CompletedTask;
159      }
160  
161      private async Task SaveChangesAsync()
162      {
163          await _savingGate.ExecuteAsync(CancellationToken.None);
164      }
165  
166      public void Dispose() => _savingGate.Dispose();
167  }