diff --git a/.gitignore b/.gitignore index 0085abe..a7e41b3 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ #VSCode .vscode/ .idea/ +*.lscache diff --git a/Changelog.md b/Changelog.md index aa11fd7..6ca59c8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,11 +1,24 @@ # Changelog +## 3.1.0 + +- :star: **Recent pages** is dead, long live **sort by last modified**. ([#32](https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote/issues/32)) + - Using the **sort by last modified** keyword, as the name implies, allows you to sort results by last modified. But unlike **recent pages** you can now use it in multiple places, i.e. with **title search**, **scoped search** and **notebook explorer**. Note it must be placed after the aforementioned keywords. + - Examples:\ + (`#` = **sort by last modified** keyword, `*` = **title search** keyword, `nb:\` = **notebook explorer** keyword, `>` = **scoped search** keyword) + - ```on #{your search query}``` + - ```on *#{your search query}``` + - ```on nb:\PathToItem\#{your search query}``` + - ```on nb:\PathToItem\>#{your search query}``` + - ```on nb:\PathToItem\*#{your search query}``` +- Added a setting to allow users to always open items in a new OneNote window. ([#40](https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote/issues/40)) + ## 3.0.1 - 2026-03-13 - Fix crash on quick note with empty title (Updated to `LinqToOneNote-2.1.1`) - Fix incorrect **scope search** check - Cache OneNote hierarchy when applicable - - This brings the performance improvement for Notebook Explorer from last version to **recent pages** and **title search** + - This brings the performance improvement from **notebook explorer** from last version to **recent pages** and **title search** ## 3.0.0 - 2026-03-04 diff --git a/Flow.Launcher.Plugin.OneNote/Icons/IconGeneratorInfo.cs b/Flow.Launcher.Plugin.OneNote/Icons/IconGeneratorInfo.cs index fdb9d41..30013e5 100644 --- a/Flow.Launcher.Plugin.OneNote/Icons/IconGeneratorInfo.cs +++ b/Flow.Launcher.Plugin.OneNote/Icons/IconGeneratorInfo.cs @@ -3,30 +3,27 @@ namespace Flow.Launcher.Plugin.OneNote.Icons { - public struct IconGeneratorInfo + public readonly struct IconGeneratorInfo { public readonly string prefix = string.Empty; public readonly Color? color; - - public IconGeneratorInfo(IOneNoteItem item) + + private IconGeneratorInfo(string prefix, Color? color = null) + { + this.prefix = prefix; + this.color = color; + } + + public static IconGeneratorInfo Create(IOneNoteItem item) { - switch (item) + return item switch { - case Notebook n: - prefix = IconConstants.Notebook; - color = n.Color; - break; - case SectionGroup sg: - prefix = sg.IsRecycleBin ? IconConstants.RecycleBin : IconConstants.SectionGroup; - break; - case Section s: - prefix = IconConstants.Section; - color = s.Color; - break; - case Page: - prefix = IconConstants.Page; - break; - } + Notebook n => new (IconConstants.Notebook, n.Color), + SectionGroup sg => new (sg.IsRecycleBin ? IconConstants.RecycleBin : IconConstants.SectionGroup), + Section s => new (IconConstants.Section, s.Color), + Page => new (IconConstants.Page), + _ => new (), + }; } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Images/logo.png b/Flow.Launcher.Plugin.OneNote/Images/logo.png index ea9fb28..db3e387 100644 Binary files a/Flow.Launcher.Plugin.OneNote/Images/logo.png and b/Flow.Launcher.Plugin.OneNote/Images/logo.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Keywords.cs b/Flow.Launcher.Plugin.OneNote/Keywords.cs index bbb74b5..f92b5b5 100644 --- a/Flow.Launcher.Plugin.OneNote/Keywords.cs +++ b/Flow.Launcher.Plugin.OneNote/Keywords.cs @@ -8,26 +8,27 @@ namespace Flow.Launcher.Plugin.OneNote public class Keywords { public const string NotebookExplorerSeparator = "\\"; - public Keyword NotebookExplorer { get; set; } = new($"nb:{NotebookExplorerSeparator}"); - public Keyword RecentPages { get; set; } = new ("rp:"); - public Keyword TitleSearch { get; set; } = new ("*"); - public Keyword ScopedSearch { get; set; } = new (">"); - + public Keyword NotebookExplorer { get; init; } = new($"nb:{NotebookExplorerSeparator}"); + [JsonPropertyName("RecentPages")] + public Keyword SortByLastModified { get; init; } = new ("rp:"); + public Keyword TitleSearch { get; init; } = new ("*"); + public Keyword ScopedSearch { get; init; } = new (">"); + + private Keyword[]? keywords; + [JsonIgnore] + public Keyword[] All => keywords ??= [NotebookExplorer, SortByLastModified, TitleSearch, ScopedSearch]; } [JsonConverter(typeof(KeywordJsonConverter))] - public class Keyword + public class Keyword(string value) { - public Keyword(string value) => Value = value; - public string Value { get; private set; } + public string Value { get; private set; } = value; public void ChangeKeyword(string newValue) => Value = newValue; public int Length => Value.Length; public static implicit operator string(Keyword keyword) => keyword.Value; public override string ToString() => Value; - - public static Keyword Empty { get; } = new (""); } //Needed for legacy as keywords where just saved as a string @@ -39,5 +40,4 @@ public override Keyword Read(ref Utf8JsonReader reader, Type typeToConvert, Json public override void Write(Utf8JsonWriter writer, Keyword value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value.Value, options); } - } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/ResultCreator.cs b/Flow.Launcher.Plugin.OneNote/ResultCreator.cs index bb954ea..856acc3 100644 --- a/Flow.Launcher.Plugin.OneNote/ResultCreator.cs +++ b/Flow.Launcher.Plugin.OneNote/ResultCreator.cs @@ -11,22 +11,12 @@ namespace Flow.Launcher.Plugin.OneNote { - public class ResultCreator + public class ResultCreator(PluginInitContext context, Settings settings, IconProvider iconProvider) { - private readonly PluginInitContext context; - private readonly Settings settings; - private readonly IconProvider iconProvider; - private const string PathSeparator = " > "; private const string BulletPoint = "\u2022 "; private const string TrianglePoint = "\u2023 "; private string ActionKeyword => context.CurrentPluginMetadata.ActionKeyword; - public ResultCreator(PluginInitContext context, Settings settings, IconProvider iconProvider) - { - this.settings = settings; - this.iconProvider = iconProvider; - this.context = context; - } private static string GetNicePath(IOneNoteItem item, string separator = PathSeparator) => item.GetRelativePath(false, separator); @@ -84,14 +74,14 @@ public List EmptyQuery() new Result { Title = "See recent pages", - SubTitle = $"Type \"{settings.Keywords.RecentPages}\" or select this option to see recently modified pages", - AutoCompleteText = $"{ActionKeyword} {settings.Keywords.RecentPages}", + SubTitle = $"Type \"{settings.Keywords.SortByLastModified}\" or select this option to see recently modified pages", + AutoCompleteText = $"{ActionKeyword} {settings.Keywords.SortByLastModified}", IcoPath = iconProvider.Recent, AddSelectedCount = false, Score = -1000, Action = _ => { - context.API.ChangeQuery($"{ActionKeyword} {settings.Keywords.RecentPages}", true); + context.API.ChangeQuery($"{ActionKeyword} {settings.Keywords.SortByLastModified}", true); return false; }, }, @@ -104,7 +94,7 @@ public List EmptyQuery() PreviewPanel = GetNewPagePreviewPanel(null, null), Action = _ => { - OneNoteApp.CreateQuickNote(OpenMode.ExistingOrNewWindow); + OneNoteApp.CreateQuickNote(settings.AlwaysOpenInNewWindow ? OpenMode.NewWindow : OpenMode.ExistingOrNewWindow); WindowHelper.FocusOneNote(); return true; }, @@ -142,7 +132,7 @@ public Result CreateOneNoteItemResult(IOneNoteItem item, bool actionIsAutoComple var toolTip = string.Empty; var subTitle = GetNicePath(item); var autoCompleteText = GetAutoCompleteText(item); - var iconInfo = new IconGeneratorInfo(item); + var iconInfo = IconGeneratorInfo.Create(item); switch (item) { @@ -213,27 +203,27 @@ public Result CreateOneNoteItemResult(IOneNoteItem item, bool actionIsAutoComple await Task.Run(() => { item.Sync(); - item.Open(); + OneNoteApp.Open(item, settings.AlwaysOpenInNewWindow); }); WindowHelper.FocusOneNote(); return true; }, }; } - - public Result CreatePageResult(Page page, string query) - => CreateOneNoteItemResult(page, false, string.IsNullOrWhiteSpace(query) ? null : context.API.FuzzySearch(query, page.Name).MatchData); - public Result CreateRecentPageResult(Page page) + public Result CreateRecentItemResult(IOneNoteItem item, bool actionIsAutoComplete, List? highlightData = null) { - var result = CreateOneNoteItemResult(page, false); - result.SubTitle = $"{page.LastModified.Humanize()} | {result.SubTitle}"; - result.IcoPath = iconProvider.Recent; + var result = CreateOneNoteItemResult(item, actionIsAutoComplete, highlightData); + result.SubTitle = string.IsNullOrWhiteSpace(result.SubTitle) + ? $"{item.LastModified.Humanize()}" + : $"{item.LastModified.Humanize()} | {result.SubTitle}"; + if(item is Page) + result.IcoPath = iconProvider.Recent; result.AddSelectedCount = false; return result; } - //When name can have invalid chars + //When new name can have invalid chars private Result CreateNewItemResult(string newName, TParent? parent, string iconPath, Func createFunc) where TNew : IOneNoteItem, INameInvalidCharacters where TParent : IOneNoteItem @@ -261,7 +251,10 @@ private Result CreateNewItemResult(string newName, TParent? paren return false; bool showOneNote = !c.SpecialKeyState.CtrlPressed; - createFunc(parent, newName, showOneNote ? OpenMode.ExistingOrNewWindow : OpenMode.None); + bool newWindow = settings.AlwaysOpenInNewWindow; + + OpenMode openMode = showOneNote ? newWindow ? OpenMode.NewWindow : OpenMode.ExistingOrNewWindow : OpenMode.None; + createFunc(parent, newName, openMode); context.API.ReQuery(); @@ -291,18 +284,27 @@ public List ContextMenu(Result selectedResult) var results = new List(); if (selectedResult.ContextData is IOneNoteItem item) { - var result = CreateOneNoteItemResult(item, false); - result.Title = $"Open and sync \"{item.Name}\""; - result.SubTitle = string.Empty; - result.Score = 30; - result.AddSelectedCount = false; - result.ContextData = null; - results.Add(result); + Result.IconDelegate icon = iconProvider.GetIcon(IconGeneratorInfo.Create(item)); + results.Add(new Result + { + Title = $"Open and sync \"{item.Name}\"", + Icon = icon, + Score = 30, + AddSelectedCount = false, + Action = _ => + { + OneNoteApp.Open(item); + OneNoteApp.SyncItem(item); + WindowHelper.FocusOneNote(); + return true; + } + }); + results.Add(new Result { Title = $"Open \"{item.Name}\" in new OneNote window", - Icon = result.Icon, + Icon = icon, Score = 20, AddSelectedCount = false, Action = _ => @@ -381,7 +383,7 @@ Result EmptyCollectionResult(string title, string iconPath, string? subTitle = n } private Lazy GetNewPagePreviewPanel(Section? section, string? pageTitle) - => new(() => new NewOneNotePagePreviewPanel(context, section, pageTitle)); + => new(() => new NewOneNotePagePreviewPanel(context, settings, section, pageTitle)); public static List NoMatchesFound() { diff --git a/Flow.Launcher.Plugin.OneNote/Search/DefaultSearch.cs b/Flow.Launcher.Plugin.OneNote/Search/DefaultSearch.cs deleted file mode 100644 index 150042f..0000000 --- a/Flow.Launcher.Plugin.OneNote/Search/DefaultSearch.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OneNoteApp = LinqToOneNote.OneNote; - -namespace Flow.Launcher.Plugin.OneNote.Search -{ - public class DefaultSearch(PluginInitContext context, Settings settings, ResultCreator resultCreator) - : SearchBase(context, settings, resultCreator, Keyword.Empty) - { - public override List GetResults(Query query) - { - string search = query.Search; - if (!char.IsLetterOrDigit(search[0])) - { - return resultCreator.InvalidQuery(); - } - - return OneNoteApp.FindPages(search) - .Select(pg => resultCreator.CreatePageResult(pg, search)) - .ToList(); - } - } -} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/NotebookExplorer.cs b/Flow.Launcher.Plugin.OneNote/Search/NotebookExplorer.cs index d7e5efc..2e8674a 100644 --- a/Flow.Launcher.Plugin.OneNote/Search/NotebookExplorer.cs +++ b/Flow.Launcher.Plugin.OneNote/Search/NotebookExplorer.cs @@ -1,46 +1,53 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using LinqToOneNote; using LinqToOneNote.Abstractions; -using OneNoteApp = LinqToOneNote.OneNote; namespace Flow.Launcher.Plugin.OneNote.Search { - public class NotebookExplorer(PluginInitContext context, Settings settings, ResultCreator resultCreator, TitleSearch titleSearch, RootCache rootCache) - : SearchBase(context, settings, resultCreator, settings.Keywords.NotebookExplorer) + public class NotebookExplorer(PluginInitContext context, Settings settings, ResultCreator resultCreator, TitleSearch titleSearch, StandardSearch standardSearch) + : SearchBase(context, settings, resultCreator) { - public override List GetResults(Query query) + private readonly Explorer explorer = new(context, settings, resultCreator); + private readonly Keywords keywords = settings.Keywords; + + protected override List GetResultsInternal(ref SearchData searchData) { - if (!ValidateSearch(query, out string? search, out IOneNoteItem? parent, out IEnumerable collection)) + if (!ValidateSearch(ref searchData)) return resultCreator.InvalidQuery(false); - List results = search switch + Keyword? keyword = SearchExtensions.GetKeyword(ref searchData, keywords); + + IOneNoteItem? parent = searchData.Parent; + SearchBase searchBase = (keyword, parent) switch { - { } when search.StartsWithOrd(Keywords.TitleSearch) && parent is not Page => titleSearch.Filter(search, parent, collection), - { } when search.StartsWithOrd(Keywords.ScopedSearch) && parent is INotebookOrSectionGroup => ScopedSearch(search, parent), - { } when !string.IsNullOrWhiteSpace(search) => Explorer(search, parent, collection), - _ => ShowAll(parent, collection), + (not null, not Page) when keyword == keywords.TitleSearch => titleSearch, + (not null, INotebookOrSectionGroup) when keyword == keywords.ScopedSearch => standardSearch, + _ => explorer, }; + var results = searchBase.GetResults(ref searchData); + if (parent == null) return results; Result result = resultCreator.CreateOneNoteItemResult(parent, false, score: Result.MaxScore); result.Title = $"Open \"{parent.Name}\" in OneNote"; - result.SubTitle = $"Use \'{Keywords.ScopedSearch}\' to search this item. Use \'{Keywords.TitleSearch}\' to search by title in this item"; + result.SubTitle = $"Use \'{keywords.ScopedSearch}\' to search this item. Use \'{keywords.TitleSearch}\' to search by title in this item. Use \'{keywords.SortByLastModified}\' to sort by recently modified"; results.Add(result); return results; } - - private bool ValidateSearch(Query query, out string? lastSearch, out IOneNoteItem? parent, out IEnumerable collection) + + private static bool ValidateSearch(ref SearchData searchData) { - lastSearch = null; - parent = null; - - collection = rootCache.Root.Notebooks; + (var search, IOneNoteItem? parent, IEnumerable? collection, _) = searchData; + + //From SearchManager.Query SearchData + //parent is null here + //collection is rootCache.Root.Notebooks as this functions validates the search with the whole query; - string search = query.Search[(query.Search.IndexOf(Keywords.NotebookExplorer, StringComparison.Ordinal) + Keywords.NotebookExplorer.Length)..]; const string separator = Keywords.NotebookExplorerSeparator; var currIndex = search.IndexOf(separator, StringComparison.Ordinal); var prevIndex = 0; @@ -48,7 +55,7 @@ private bool ValidateSearch(Query query, out string? lastSearch, out IOneNoteIte while (currIndex != -1) { var itemName = search[prevIndex..currIndex]; - parent = collection.FirstOrDefault(item => item.Name == itemName); + parent = collection?.FirstOrDefault(item => item.Name == itemName); if (parent == null) return false; @@ -58,69 +65,78 @@ private bool ValidateSearch(Query query, out string? lastSearch, out IOneNoteIte currIndex = search.IndexOf(separator, currIndex + separator.Length, StringComparison.Ordinal); } - lastSearch = search[prevIndex..]; + searchData.Search = search[prevIndex..]; + searchData.Parent = parent; + searchData.Collection = collection; return true; } + + protected override List EmptySearch(ref SearchData searchData) => explorer.GetResults(ref searchData); - private List ShowAll(IOneNoteItem? parent, IEnumerable collection) - { - var results = collection.FilterBySettings(settings) - .Select(item => resultCreator.CreateOneNoteItemResult(item, true)) - .ToList(); - - return results.Count != 0 ? results : resultCreator.EmptyCollection(results, parent); - } - - private List ScopedSearch(string query, IOneNoteItem parent) + private class Explorer(PluginInitContext context, Settings settings, ResultCreator resultCreator) : SearchBase(context, settings, resultCreator) { - if (query.Length == Keywords.ScopedSearch.Length) - return resultCreator.SearchType("Now searching all pages", parent.Name); + protected override List GetResultsInternal(ref SearchData searchData) + { + var (search, parent, collection, sortOrder) = searchData; - if (!char.IsLetterOrDigit(query[Keywords.ScopedSearch.Length])) - return resultCreator.InvalidQuery(); + Debug.Assert(collection != null, nameof(collection) + " != null"); + + var searchResults = collection.FilterBySettings(settings).FuzzySearch(search, context); + var results = sortOrder switch + { + SortOrder.RecentlyModified => searchResults.OrderByDescending(sr => sr.Item.LastModified) + .Select(sr => resultCreator.CreateRecentItemResult(sr.Item, true, sr.HighlightData)) + .ToList(), + _ => searchResults.Select(sr => resultCreator.CreateOneNoteItemResult(sr.Item, true, sr.HighlightData, sr.Score)) + .ToList(), + }; - string currentSearch = query[Keywords.ScopedSearch.Length..]; + // If parent is a section, pages inside can have the same name + if (parent is not Section && results.Any(result => string.Equals(search.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) + return results; - var results = OneNoteApp.FindPages(currentSearch, parent) - .Select(pg => resultCreator.CreatePageResult(pg, currentSearch)) - .ToList(); + if (parent?.IsInRecycleBin() == true) + return results; - return results.Count != 0 ? results : ResultCreator.NoMatchesFound(); - } - - private List Explorer(string search, IOneNoteItem? parent, IEnumerable collection) - { - var results = collection.FilterBySettings(settings) - .FuzzySearch(search, context) - .Select(r => resultCreator.CreateOneNoteItemResult(r.Item, true, r.HighlightData, r.Score)) - .ToList(); + //Add option to create new items + switch (parent) + { + case null: + results.Add(resultCreator.CreateNewNotebookResult(search)); + break; + case INotebookOrSectionGroup x: + results.Add(resultCreator.CreateNewSectionResult(search, x)); + results.Add(resultCreator.CreateNewSectionGroupResult(search, x)); + break; + case Section section: + if (!section.Locked) + { + results.Add(resultCreator.CreateNewPageResult(search, section)); + } + break; + } - // If parent is a section, pages inside can have the same name - if (parent is not Section && results.Any(result => string.Equals(search.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) - return results; - - if (parent?.IsInRecycleBin() == true) return results; + } - //Add option to create new items - switch (parent) + protected override List EmptySearch(ref SearchData searchData) { - case null: - results.Add(resultCreator.CreateNewNotebookResult(search)); - break; - case INotebookOrSectionGroup x: - results.Add(resultCreator.CreateNewSectionResult(search, x)); - results.Add(resultCreator.CreateNewSectionGroupResult(search, x)); - break; - case Section section: - if (!section.Locked) - { - results.Add(resultCreator.CreateNewPageResult(search, section)); - } - break; + var (_, parent, collection, sortOrder) = searchData; + + Debug.Assert(collection != null); + var results = sortOrder switch + { + SortOrder.RecentlyModified => collection.FilterBySettings(settings) + .OrderByDescending(item => item.LastModified) + .Select(item => resultCreator.CreateRecentItemResult(item, true)) + .ToList(), + _ => collection.FilterBySettings(settings) + .Select(item => resultCreator.CreateOneNoteItemResult(item, true)) + .ToList(), + }; + + return results.Count != 0 ? results : resultCreator.EmptyCollection(results, parent); } - - return results; } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/RecentPages.cs b/Flow.Launcher.Plugin.OneNote/Search/RecentPages.cs deleted file mode 100644 index 41f7129..0000000 --- a/Flow.Launcher.Plugin.OneNote/Search/RecentPages.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using LinqToOneNote; - -namespace Flow.Launcher.Plugin.OneNote.Search -{ - public class RecentPages(PluginInitContext context, Settings settings, ResultCreator resultCreator, RootCache rootCache) - : SearchBase(context, settings, resultCreator, settings.Keywords.RecentPages) - { - public override List GetResults(Query query) - { - int count = settings.DefaultRecentsCount; - string search = query.Search; - if (search.Length > keyword.Length && int.TryParse(search[keyword.Length..], out int userChosenCount)) - count = userChosenCount; - - return rootCache.Root - .Notebooks - .GetAllPages() - .FilterBySettings(settings) - .OrderByDescending(pg => pg.LastModified) - .Take(count) - .Select(resultCreator.CreateRecentPageResult) - .ToList(); - } - } -} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/SearchBase.cs b/Flow.Launcher.Plugin.OneNote/Search/SearchBase.cs index 12bc90f..67cbc0a 100644 --- a/Flow.Launcher.Plugin.OneNote/Search/SearchBase.cs +++ b/Flow.Launcher.Plugin.OneNote/Search/SearchBase.cs @@ -2,13 +2,17 @@ namespace Flow.Launcher.Plugin.OneNote.Search { - public abstract class SearchBase(PluginInitContext context, Settings settings, ResultCreator resultCreator, Keyword keyword) + public abstract class SearchBase(PluginInitContext context, Settings settings, ResultCreator resultCreator) { - protected readonly PluginInitContext context = context; - protected readonly Settings settings = settings; protected readonly ResultCreator resultCreator = resultCreator; - public readonly Keyword keyword = keyword; - protected Keywords Keywords => settings.Keywords; - public abstract List GetResults(Query query); + protected readonly Settings settings = settings; + protected readonly PluginInitContext context = context; + protected abstract List GetResultsInternal(ref SearchData searchData); + protected abstract List EmptySearch(ref SearchData searchData); + + public List GetResults(ref SearchData searchData) => + string.IsNullOrWhiteSpace(searchData.Search) + ? EmptySearch(ref searchData) + : GetResultsInternal(ref searchData); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/SearchExtensions.cs b/Flow.Launcher.Plugin.OneNote/Search/SearchExtensions.cs index 20ac402..08ee964 100644 --- a/Flow.Launcher.Plugin.OneNote/Search/SearchExtensions.cs +++ b/Flow.Launcher.Plugin.OneNote/Search/SearchExtensions.cs @@ -1,14 +1,27 @@ using System; using System.Collections.Generic; +using System.Linq; using Flow.Launcher.Plugin.SharedModels; using LinqToOneNote; namespace Flow.Launcher.Plugin.OneNote.Search { - public record struct SearchResult(T Item, List? HighlightData, int Score) where T : IOneNoteItem; + public enum SortOrder + { + Default = 0, + RecentlyModified = 1, + } + + //Parent => parent of the current item/search area + //Collection => children of the parent, if parent == null; Collection = rootCache.Root.Notebooks + public record struct SearchData(string Search, IOneNoteItem? Parent, IEnumerable? Collection, SortOrder SortOrder); + + public record SearchResult(T Item, List? HighlightData, int Score) where T : IOneNoteItem; + public static class SearchExtensions { - public static IEnumerable> FuzzySearch(this IEnumerable source, string search, PluginInitContext context) where T: IOneNoteItem + public static IEnumerable> FuzzySearch(this IEnumerable source, string search, PluginInitContext context) + where T : IOneNoteItem { foreach (var item in source) { @@ -19,31 +32,63 @@ public static IEnumerable> FuzzySearch(this IEnumerable so } } } + public static IEnumerable FilterBySettings(this IEnumerable source, Settings settings) where T : IOneNoteItem { - foreach (var item in source) + return source.Where(item => { - var success = true; if (!settings.ShowEncrypted && item is Section section) { - success = !section.Encrypted; + return !section.Encrypted; } - if (!settings.ShowRecycleBin && item.IsInRecycleBin()) - { - success = false; - } + return settings.ShowRecycleBin || !item.IsInRecycleBin(); + }); + } + + public static IEnumerable Take(this IEnumerable source, Settings settings) => + settings.UseMaxResults + ? source.Take(settings.MaxResults) + : source; + + /// + /// Gets the keyword from the search, and removes it from the search string. Also gets the sort order + /// + /// + /// + /// + public static Keyword? GetKeyword(ref SearchData searchData, Keywords keywords) + { + Keyword? keyword = null; + var searchSpan = searchData.Search.AsSpan(); + + for (var i = 0; i < keywords.All.Length; i++) + { + Keyword key = keywords.All[i]; + if (!StartsWithKeyword(ref searchSpan, key)) + continue; + + keyword = key; + break; + } + + if (keyword == keywords.SortByLastModified || StartsWithKeyword(ref searchSpan, keywords.SortByLastModified)) + searchData.SortOrder = SortOrder.RecentlyModified; - if (success) + searchData.Search = searchSpan.ToString(); + return keyword; + + //Check if starts with keyword and if so trim the keyword + static bool StartsWithKeyword(ref ReadOnlySpan searchSpan, Keyword keyword) + { + if (searchSpan.StartsWith(keyword.Value.AsSpan(), StringComparison.Ordinal)) { - yield return item; + searchSpan = searchSpan[keyword.Length..]; + return true; } - } - } - public static bool StartsWithOrd(this string str, string value) - { - return str.StartsWith(value, StringComparison.Ordinal); + return false; + } } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/SearchManager.cs b/Flow.Launcher.Plugin.OneNote/Search/SearchManager.cs index ed56c56..8d8683d 100644 --- a/Flow.Launcher.Plugin.OneNote/Search/SearchManager.cs +++ b/Flow.Launcher.Plugin.OneNote/Search/SearchManager.cs @@ -4,36 +4,42 @@ namespace Flow.Launcher.Plugin.OneNote.Search { public class SearchManager { - private readonly TitleSearch titleSearch; + private readonly Keywords keywords; private readonly NotebookExplorer notebookExplorer; - private readonly DefaultSearch defaultSearch; - private readonly RecentPages recentPages; - private readonly RootCache rootCache; - public RootCache RootCache => rootCache; + private readonly StandardSearch standardSearch; + private readonly TitleSearch titleSearch; + public RootCache RootCache { get; } public SearchManager(PluginInitContext context, Settings settings, ResultCreator resultCreator) { - rootCache = new RootCache(); - titleSearch = new TitleSearch(context, settings, resultCreator, rootCache); - notebookExplorer = new NotebookExplorer(context, settings, resultCreator, titleSearch, rootCache); - recentPages = new RecentPages(context, settings, resultCreator, rootCache); - defaultSearch = new DefaultSearch(context, settings, resultCreator); + standardSearch = new StandardSearch(context, settings, resultCreator); + titleSearch = new TitleSearch(context, settings, resultCreator); + notebookExplorer = new NotebookExplorer(context, settings, resultCreator, titleSearch, standardSearch); + + RootCache = new RootCache(); + keywords = settings.Keywords; } public List Query(Query query) { if (query.IsReQuery) { - rootCache.SetDirty(); + RootCache.SetDirty(); } - string search = query.Search; - return search switch + + var searchData = new SearchData(query.Search, null, RootCache.Root.Notebooks, SortOrder.Default); + Keyword? keyword = SearchExtensions.GetKeyword(ref searchData, keywords); + + SearchBase searchBase = keyword switch { - { } when search.StartsWithOrd(titleSearch.keyword) => titleSearch.GetResults(query), - { } when search.StartsWithOrd(notebookExplorer.keyword) => notebookExplorer.GetResults(query), - { } when search.StartsWithOrd(recentPages.keyword) => recentPages.GetResults(query), - _ => defaultSearch.GetResults(query), + not null when keyword == keywords.NotebookExplorer => notebookExplorer, + not null when keyword == keywords.TitleSearch => titleSearch, + _ => standardSearch, }; + + var results = searchBase.GetResults(ref searchData); + + return results.Count != 0 ? results : ResultCreator.NoMatchesFound(); } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/StandardSearch.cs b/Flow.Launcher.Plugin.OneNote/Search/StandardSearch.cs new file mode 100644 index 0000000..f8e0ed7 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/Search/StandardSearch.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using LinqToOneNote; + +namespace Flow.Launcher.Plugin.OneNote.Search +{ + public class StandardSearch(PluginInitContext context, Settings settings, ResultCreator resultCreator) : SearchBase(context, settings, resultCreator) + { + protected override List GetResultsInternal(ref SearchData searchData) + { + var (search, parent, _, sortOrder) = searchData; + + if (!char.IsLetterOrDigit(search[0])) + return resultCreator.InvalidQuery(); + + IEnumerable pages = parent is null + ? LinqToOneNote.OneNote.FindPages(search) + : LinqToOneNote.OneNote.FindPages(search, parent); + + return sortOrder switch + { + SortOrder.RecentlyModified => pages.OrderByDescending(pg => pg.LastModified) + .Take(settings) + .Select(pg => resultCreator.CreateRecentItemResult(pg, false)) + .ToList(), + _ => pages.Take(settings) + .Select(pg => resultCreator.CreateOneNoteItemResult(pg, false, + string.IsNullOrWhiteSpace(search) + ? null + : context.API.FuzzySearch(search, pg.Name).MatchData)) + .ToList() + }; + } + + protected override List EmptySearch(ref SearchData searchData) + { + return searchData.SortOrder switch + { + SortOrder.RecentlyModified => searchData.Collection + .GetAllPages() + .FilterBySettings(settings) + .OrderByDescending(pg => pg.LastModified) + .Take(settings) + .Select(pg => resultCreator.CreateRecentItemResult(pg, false)) + .ToList(), + _ => resultCreator.SearchType("Now searching all pages", searchData.Parent?.Name), + }; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Search/TitleSearch.cs b/Flow.Launcher.Plugin.OneNote/Search/TitleSearch.cs index a6649fe..1f5ae54 100644 --- a/Flow.Launcher.Plugin.OneNote/Search/TitleSearch.cs +++ b/Flow.Launcher.Plugin.OneNote/Search/TitleSearch.cs @@ -4,25 +4,35 @@ namespace Flow.Launcher.Plugin.OneNote.Search { - public class TitleSearch(PluginInitContext context, Settings settings, ResultCreator resultCreator, RootCache rootCache) - : SearchBase(context, settings, resultCreator, settings.Keywords.TitleSearch) + public class TitleSearch(PluginInitContext context, Settings settings, ResultCreator resultCreator) : SearchBase(context, settings, resultCreator) { - public override List GetResults(Query query) => Filter(query.Search, null, rootCache.Root.Notebooks); - - public List Filter(string query, IOneNoteItem? parent, IEnumerable collection) + protected override List GetResultsInternal(ref SearchData searchData) { - if (query.Length == keyword.Length) - return resultCreator.SearchType("Now searching by title", parent?.Name); - - var currentSearch = query[keyword.Length..]; - + var (search, _, collection, sortOrder) = searchData; var results = collection.Descendants() .FilterBySettings(settings) - .FuzzySearch(currentSearch, context) - .Select(x => resultCreator.CreateOneNoteItemResult(x.Item, false, x.HighlightData, x.Score)) - .ToList(); + .FuzzySearch(search, context); - return results.Count != 0 ? results : ResultCreator.NoMatchesFound(); + return sortOrder switch + { + SortOrder.RecentlyModified => results.OrderByDescending(sr => sr.Item.LastModified) + .Take(settings) + .Select(sr => resultCreator.CreateRecentItemResult(sr.Item, false, sr.HighlightData)) + .ToList(), + _ => results.Take(settings) + .Select(sr => resultCreator.CreateOneNoteItemResult(sr.Item, false, sr.HighlightData, sr.Score)) + .ToList(), + }; + } + + protected override List EmptySearch(ref SearchData searchData) + { + const string emptySearchPrefix = "Now searching by title"; + return searchData.SortOrder switch + { + SortOrder.RecentlyModified => resultCreator.SearchType($"{emptySearchPrefix} ordered by last modified", searchData.Parent?.Name), + _ => resultCreator.SearchType(emptySearchPrefix, searchData.Parent?.Name), + }; } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Settings.cs b/Flow.Launcher.Plugin.OneNote/Settings.cs index 109321d..455cbe0 100644 --- a/Flow.Launcher.Plugin.OneNote/Settings.cs +++ b/Flow.Launcher.Plugin.OneNote/Settings.cs @@ -2,46 +2,64 @@ namespace Flow.Launcher.Plugin.OneNote { - public class Settings : UI.Model - { - private bool showUnread = true; - private int defaultRecentsCount = 5; - private bool showRecycleBin = true; - private bool showEncrypted; - private bool createColoredIcons = true; - private IconTheme iconTheme = IconTheme.Color; - public Keywords Keywords { get; init; } = new Keywords(); - - public bool ShowRecycleBin - { - get => showRecycleBin; - set => SetProperty(ref showRecycleBin, value); - } - public bool ShowUnread - { - get => showUnread; - set => SetProperty(ref showUnread, value); - } - public int DefaultRecentsCount - { - get => defaultRecentsCount; - set => SetProperty(ref defaultRecentsCount, value); - } - public bool ShowEncrypted - { - get => showEncrypted; - set => SetProperty(ref showEncrypted, value); - } - public bool CreateColoredIcons - { - get => createColoredIcons; - set => SetProperty(ref createColoredIcons, value); - } - - public IconTheme IconTheme - { - get => iconTheme; - set => SetProperty(ref iconTheme, value); - } - } -} + public class Settings : UI.Model + { + private bool showUnread = true; + private bool showRecycleBin = true; + private bool showEncrypted; + private bool createColoredIcons = true; + private int maxResults = 30; + private bool useMaxResults = true; + private bool alwaysOpenInNewWindow = false; + private IconTheme iconTheme = IconTheme.Color; + public Keywords Keywords { get; init; } = new Keywords(); + + public bool ShowRecycleBin + { + get => showRecycleBin; + set => SetProperty(ref showRecycleBin, value); + } + + public bool ShowUnread + { + get => showUnread; + set => SetProperty(ref showUnread, value); + } + + public int MaxResults + { + get => maxResults; + set => SetProperty(ref maxResults, value); + } + + public bool UseMaxResults + { + get => useMaxResults; + set => SetProperty(ref useMaxResults, value); + } + + public bool ShowEncrypted + { + get => showEncrypted; + set => SetProperty(ref showEncrypted, value); + } + + public bool CreateColoredIcons + { + get => createColoredIcons; + set => SetProperty(ref createColoredIcons, value); + } + + public IconTheme IconTheme + { + get => iconTheme; + set => SetProperty(ref iconTheme, value); + } + + public bool AlwaysOpenInNewWindow + { + get => alwaysOpenInNewWindow; + set => alwaysOpenInNewWindow = value; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/UI/Model.cs b/Flow.Launcher.Plugin.OneNote/UI/Model.cs index 71699e8..5379f54 100644 --- a/Flow.Launcher.Plugin.OneNote/UI/Model.cs +++ b/Flow.Launcher.Plugin.OneNote/UI/Model.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Flow.Launcher.Plugin.OneNote.UI { @@ -6,7 +7,7 @@ public class Model : BaseModel { protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) { - if (Equals(field, newValue)) + if (EqualityComparer.Default.Equals(field, newValue)) return false; field = newValue; diff --git a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/NewOneNotePageViewModel.cs b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/NewOneNotePageViewModel.cs index b70c486..9c6adfb 100644 --- a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/NewOneNotePageViewModel.cs +++ b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/NewOneNotePageViewModel.cs @@ -12,10 +12,12 @@ public class NewOneNotePageViewModel : Model private string pageContent = string.Empty; private readonly Section? section; private readonly PluginInitContext context; + private readonly Settings settings; - public NewOneNotePageViewModel(PluginInitContext context, Section? section, string? pageTitle) + public NewOneNotePageViewModel(PluginInitContext context, Settings settings, Section? section, string? pageTitle) { this.context = context; + this.settings = settings; this.section = section; PageTitle = pageTitle; CreateCommand = new RelayCommand(() => CreatePage(false)); @@ -33,6 +35,7 @@ private void CreatePage(bool openImmediately) { page = OneNoteApp.CreatePage(section, PageTitle); } + var xmlWrap = $""" @@ -52,7 +55,7 @@ private void CreatePage(bool openImmediately) context.API.ReQuery(); if (openImmediately) { - page.Open(); + OneNoteApp.Open(page, settings.AlwaysOpenInNewWindow); context.API.HideMainWindow(); WindowHelper.FocusOneNote(); } diff --git a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs index 91e153a..5b305c5 100644 --- a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs +++ b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs @@ -21,6 +21,7 @@ public SettingsViewModel(PluginInitContext context, Settings settings, IconProvi Keywords = settings.Keywords //Order is the order they are written in Keywords.cs .GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType == typeof(Keyword)) .Select(p => new KeywordViewModel(p.Name.Humanize(LetterCasing.Title), (Keyword)p.GetValue(settings.Keywords)!)) .ToArray(); IconThemes = Enum.GetValues() @@ -61,7 +62,6 @@ public SettingsViewModel(PluginInitContext context, Settings settings, IconProvi public Settings Settings { get; } public KeywordViewModel[] Keywords { get; } public KeywordViewModel NotebookExplorerKeyword => Keywords[0]; - public KeywordViewModel RecentPagesKeyword => Keywords[1]; public IconThemeViewModel[] IconThemes { get; } public string CachedIconsFileSize => iconProvider.GeneratedImagesDirectoryInfo.EnumerateFiles() .Select(file => file.Length) diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/NewOneNotePagePreviewPanel.xaml.cs b/Flow.Launcher.Plugin.OneNote/UI/Views/NewOneNotePagePreviewPanel.xaml.cs index 4d000ed..a36608a 100644 --- a/Flow.Launcher.Plugin.OneNote/UI/Views/NewOneNotePagePreviewPanel.xaml.cs +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/NewOneNotePagePreviewPanel.xaml.cs @@ -8,9 +8,9 @@ namespace Flow.Launcher.Plugin.OneNote.UI.Views { public partial class NewOneNotePagePreviewPanel : UserControl { - public NewOneNotePagePreviewPanel(PluginInitContext context, Section? section, string? pageTitle) + public NewOneNotePagePreviewPanel(PluginInitContext context, Settings settings, Section? section, string? pageTitle) { - DataContext = new NewOneNotePageViewModel(context, section, pageTitle); + DataContext = new NewOneNotePageViewModel(context, settings, section, pageTitle); InitializeComponent(); } private void NewOneNotePagePreviewPanel_OnLoaded(object sender, RoutedEventArgs e) diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml index 0b7745d..aa1e65f 100644 --- a/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml @@ -19,15 +19,6 @@ - - - 1 - 20 - - @@ -85,6 +76,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -165,28 +197,6 @@ - - - - - - - - - - - - - - +

OneNote for Flow Launcher

@@ -24,11 +24,11 @@ A [OneNote](https://www.microsoft.com/en-gb/microsoft-365/onenote/digital-note-t - [Installation](#installation) - [Features](#features) - [At a Glance](#at-a-glance) - - [New with Version 2.1.0](#new-with-version-210) - [Default Search](#default-search) - [Notebook Explorer](#notebook-explorer) - [Create New Items](#create-new-items) - - [Recent Pages](#recent-pages) + - [Create Pages](#create-pages) + - [Sort by Last Modified](#sort-by-last-modified) - [Scoped Search](#scoped-search) - [Title Search](#title-search) - [Settings](#settings) @@ -48,7 +48,7 @@ pm install OneNote > [!IMPORTANT] > > - [Versions `2.0+`](Changelog.md#200---2023-10-05) requires Flow Launcher version `1.16+`. For earlier versions see [releases](https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote/releases). -> - [Versions `3.0+`](Changelog.md#300---2026-03-04) requires Flow Launcher version `2.1+`. +> - [Versions `3.0+`](Changelog.md#300---2026-03-04) requires Flow Launcher version `2.1+`. > - This plugin is local only! It requires an installation of OneNote on you system! > - Some features require Windows/Microsoft Search to be enabled on your system. See more info [here](#additional-information). @@ -60,23 +60,21 @@ pm install OneNote | -------------------------- | --------------------------------------- | ---------------------------- | | ` on {your search query} ` | [Default Search](#default-search) | Search OneNote pages | | ` on nb:\ ` | [Notebook Explorer](#notebook-explorer) | Navigate notebooks hierarchy | -| ` on rp: ` | [Recent Pages](#recent-pages) | View recently modified pages | #### Modifiers -| Keyword | Name | Notes | -| ------- | ------------------------------- | -------------------------------------------- | -| ` > ` | [Scoped Search](#scoped-search) | Search only within a specific hierarchy item | -| ` * ` | [Title Search](#title-search) | Search only the title of hierarchy items | +| Keyword | Name | Notes | +| ------- | ----------------------------------------------- | -------------------------------------------- | +| ` > ` | [Scoped Search](#scoped-search) | Search only within a specific hierarchy item | +| ` * ` | [Title Search](#title-search) | Search only the title of hierarchy items | +| ` # ` | [Sort by Last Modified](#sort-by-last-modified) | Sort results by last modified | > [!NOTE] > Hierarchy items are notebooks, section groups, sections and pages. -### New with Version 2.1.0 - -- :star: [New page preview panel](#create-pages) :star: for quickly creating pages with text! -- Added icons for different themes. The themes available are color, light, dark and one that matches Flow Launcher's current theme. Change it in the settings! +> [!NOTE] +> Keywords are customisable in the [settings](#settings). ### Default Search @@ -140,17 +138,30 @@ This preview panel is available on all *create a page* results in the plugin and ![create pages gif](doc/create_page.gif) -### Recent Pages +### Sort by Last Modified ``` -on rp: +on #{your search query} ``` -Displays your recently modified OneNote pages. +Displays your results sorted by last modified. -Add a number after `` rp: `` to display that number of recent pages. E.g. the full query ``on rp:10`` will show the 10 most recently modified pages. +Can be used with [default search](#default-search), [notebook explorer](#notebook-explorer), [title search](#title-search) and [scoped search](#scoped-search). -![recent pages gif](doc/recent_pages.gif) +
+

Examples

+ +- ```on #{your search query}``` +- ```on *#{your search query}``` +- ```on nb:\PathToItem\#{your search query}``` +- ```on nb:\PathToItem\>#{your search query}``` +- ```on nb:\PathToItem\*#{your search query}``` + +
+ + +> [!IMPORTANT] +> The sort by last modified keyword must be placed after other keywords but before your search. ### Scoped Search @@ -224,7 +235,7 @@ All the keywords used can be changed according to user preference. ## Changelog -See [here](Changelog.md) for the full list of changes. +See [Changelog.md](Changelog.md) for the full list of changes. ## Additional Information diff --git a/doc/keywords.png b/doc/keywords.png index 1231593..1d59a9d 100644 Binary files a/doc/keywords.png and b/doc/keywords.png differ diff --git a/doc/onenote.png b/doc/onenote.png deleted file mode 100644 index 2fec1ba..0000000 Binary files a/doc/onenote.png and /dev/null differ diff --git a/doc/settings.png b/doc/settings.png index 5f3ac27..e63483e 100644 Binary files a/doc/settings.png and b/doc/settings.png differ