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 }