BookmarksCommandProvider.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.Diagnostics.Contracts; 6 using System.IO; 7 using System.Linq; 8 using System.Threading; 9 using Microsoft.CmdPal.Ext.Bookmarks.Pages; 10 using Microsoft.CmdPal.Ext.Bookmarks.Persistence; 11 using Microsoft.CmdPal.Ext.Bookmarks.Services; 12 using Microsoft.CommandPalette.Extensions; 13 14 namespace Microsoft.CmdPal.Ext.Bookmarks; 15 16 public sealed partial class BookmarksCommandProvider : CommandProvider 17 { 18 private const int LoadStateNotLoaded = 0; 19 private const int LoadStateLoading = 1; 20 private const int LoadStateLoaded = 2; 21 22 private readonly IPlaceholderParser _placeholderParser = new PlaceholderParser(); 23 private readonly IBookmarksManager _bookmarksManager; 24 private readonly IBookmarkResolver _commandResolver; 25 private readonly IBookmarkIconLocator _iconLocator = new IconLocator(); 26 27 private readonly ListItem _addNewItem; 28 private readonly Lock _bookmarksLock = new(); 29 30 private ICommandItem[] _commands = []; 31 private List<BookmarkListItem> _bookmarks = []; 32 private int _loadState; 33 34 private static string StateJsonPath() 35 { 36 var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); 37 Directory.CreateDirectory(directory); 38 return Path.Combine(directory, "bookmarks.json"); 39 } 40 41 public static BookmarksCommandProvider CreateWithDefaultStore() 42 { 43 return new BookmarksCommandProvider(new BookmarksManager(new FileBookmarkDataSource(StateJsonPath()))); 44 } 45 46 internal BookmarksCommandProvider(IBookmarksManager bookmarksManager) 47 { 48 ArgumentNullException.ThrowIfNull(bookmarksManager); 49 _bookmarksManager = bookmarksManager; 50 _bookmarksManager.BookmarkAdded += OnBookmarkAdded; 51 _bookmarksManager.BookmarkRemoved += OnBookmarkRemoved; 52 53 _commandResolver = new BookmarkResolver(_placeholderParser); 54 55 Id = "Bookmarks"; 56 DisplayName = Resources.bookmarks_display_name; 57 Icon = Icons.PinIcon; 58 59 var addBookmarkPage = new AddBookmarkPage(null); 60 addBookmarkPage.AddedCommand += (_, e) => _bookmarksManager.Add(e.Name, e.Bookmark); 61 _addNewItem = new ListItem(addBookmarkPage); 62 } 63 64 private void OnBookmarkAdded(BookmarkData bookmarkData) 65 { 66 var newItem = new BookmarkListItem(bookmarkData, _bookmarksManager, _commandResolver, _iconLocator, _placeholderParser); 67 lock (_bookmarksLock) 68 { 69 _bookmarks.Add(newItem); 70 } 71 72 NotifyChange(); 73 } 74 75 private void OnBookmarkRemoved(BookmarkData bookmarkData) 76 { 77 lock (_bookmarksLock) 78 { 79 _bookmarks.RemoveAll(t => t.BookmarkId == bookmarkData.Id); 80 } 81 82 NotifyChange(); 83 } 84 85 public override ICommandItem[] TopLevelCommands() 86 { 87 if (Volatile.Read(ref _loadState) != LoadStateLoaded) 88 { 89 if (Interlocked.CompareExchange(ref _loadState, LoadStateLoading, LoadStateNotLoaded) == LoadStateNotLoaded) 90 { 91 try 92 { 93 lock (_bookmarksLock) 94 { 95 _bookmarks = [.. _bookmarksManager.Bookmarks.Select(bookmark => new BookmarkListItem(bookmark, _bookmarksManager, _commandResolver, _iconLocator, _placeholderParser))]; 96 _commands = BuildTopLevelCommandsUnsafe(); 97 } 98 99 Volatile.Write(ref _loadState, LoadStateLoaded); 100 RaiseItemsChanged(); 101 } 102 catch 103 { 104 Volatile.Write(ref _loadState, LoadStateNotLoaded); 105 throw; 106 } 107 } 108 } 109 110 return _commands; 111 } 112 113 private void NotifyChange() 114 { 115 if (Volatile.Read(ref _loadState) != LoadStateLoaded) 116 { 117 return; 118 } 119 120 lock (_bookmarksLock) 121 { 122 _commands = BuildTopLevelCommandsUnsafe(); 123 } 124 125 RaiseItemsChanged(); 126 } 127 128 [Pure] 129 private ICommandItem[] BuildTopLevelCommandsUnsafe() => [_addNewItem, .. _bookmarks]; 130 }