From 0e886851407a13ebc2660f25085969de91b5563e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Sun, 28 Nov 2021 21:50:21 +0100 Subject: [PATCH] Make filename take precedence over folders when matching sub-paths (#112) --- CHANGELOG.md | 7 +- internal/adapter/lsp/server.go | 4 +- internal/adapter/sqlite/note_dao.go | 194 +++++++++++------- internal/adapter/sqlite/note_dao_test.go | 143 +++++++++---- .../adapter/sqlite/testdata/default/notes.yml | 13 ++ internal/cli/filtering.go | 12 +- internal/core/note_find.go | 55 ++--- internal/core/notebook.go | 9 +- 8 files changed, 277 insertions(+), 160 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790b2f5..599cecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ All notable changes to this project will be documented in this file. - +## Unreleased + +### Fixed + +* [#111](https://github.com/mickael-menu/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links. + ## 0.8.0 diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go index 232e9cf..75ee984 100644 --- a/internal/adapter/lsp/server.go +++ b/internal/adapter/lsp/server.go @@ -428,7 +428,7 @@ func NewServer(opts ServerOpts) *Server { } opts := core.NoteFindOpts{ - LinkTo: &core.LinkFilter{Paths: []string{p}}, + LinkTo: &core.LinkFilter{Hrefs: []string{p}}, } notes, err := notebook.FindNotes(opts) @@ -567,7 +567,7 @@ func (s *Server) executeCommandNew(context *glsp.Context, args []interface{}) (i return nil, err } note, err = notebook.FindNote(core.NoteFindOpts{ - IncludePaths: []string{noteExists.Name}, + IncludeHrefs: []string{noteExists.Name}, }) if err != nil { return nil, err diff --git a/internal/adapter/sqlite/note_dao.go b/internal/adapter/sqlite/note_dao.go index 5487022..30a32a8 100644 --- a/internal/adapter/sqlite/note_dao.go +++ b/internal/adapter/sqlite/note_dao.go @@ -24,12 +24,13 @@ type NoteDAO struct { logger util.Logger // Prepared SQL statements - indexedStmt *LazyStmt - addStmt *LazyStmt - updateStmt *LazyStmt - removeStmt *LazyStmt - findIdByPathStmt *LazyStmt - findByIdStmt *LazyStmt + indexedStmt *LazyStmt + addStmt *LazyStmt + updateStmt *LazyStmt + removeStmt *LazyStmt + findIdByPathStmt *LazyStmt + findIdsByPathRegexStmt *LazyStmt + findByIdStmt *LazyStmt } // NewNoteDAO creates a new instance of a DAO working on the given database @@ -70,6 +71,15 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO { WHERE path = ? `), + // Find note IDs from a regex matching their path. + findIdsByPathRegexStmt: tx.PrepareLazy(` + SELECT id FROM notes + WHERE path REGEXP ? + -- To find the best match possible, we sort by path length. + -- See https://github.com/mickael-menu/zk/issues/23 + ORDER BY LENGTH(path) ASC + `), + // Find a note from its ID. findByIdStmt: tx.PrepareLazy(` SELECT id, path, title, lead, body, raw_content, word_count, created, modified, metadata, checksum, tags, lead AS snippet @@ -195,49 +205,49 @@ func (d *NoteDAO) FindIdByPath(path string) (core.NoteID, error) { return idForRow(row) } -func (d *NoteDAO) FindIdsByHrefs(hrefs []string, allowPartialMatch bool) ([]core.NoteID, error) { - ids := make([]core.NoteID, 0) - for _, href := range hrefs { - id, err := d.FindIdByHref(href, allowPartialMatch) - if err != nil { - return ids, err - } - if id.IsValid() { - ids = append(ids, id) - } - } +func idForRow(row *sql.Row) (core.NoteID, error) { + var id sql.NullInt64 + err := row.Scan(&id) - if len(ids) == 0 { - return ids, fmt.Errorf("could not find notes at: " + strings.Join(hrefs, ", ")) + switch { + case err == sql.ErrNoRows: + return 0, nil + case err != nil: + return 0, err + default: + return core.NoteID(id.Int64), nil } - return ids, nil } -func (d *NoteDAO) FindIdByHref(href string, allowPartialMatch bool) (core.NoteID, error) { - if allowPartialMatch { - id, err := d.FindIdByHref(href, false) - if id.IsValid() || err != nil { - return id, err - } - } - - opts := core.NewNoteFindOptsByHref(href, allowPartialMatch) - - rows, err := d.findRows(opts, noteSelectionID) +func (d *NoteDAO) findIdsByPathRegex(regex string) ([]core.NoteID, error) { + ids := []core.NoteID{} + rows, err := d.findIdsByPathRegexStmt.Query(regex) if err != nil { - return 0, err + return ids, err } defer rows.Close() for rows.Next() { - return d.scanNoteID(rows) + var id sql.NullInt64 + err := rows.Scan(&id) + if err != nil { + return ids, err + } + + ids = append(ids, core.NoteID(id.Int64)) } - return 0, nil + + return ids, nil } -func idForRow(row *sql.Row) (core.NoteID, error) { +func (d *NoteDAO) findIdWithStmt(stmt *LazyStmt, args ...interface{}) (core.NoteID, error) { + row, err := stmt.QueryRow(args...) + if err != nil { + return core.NoteID(0), err + } + var id sql.NullInt64 - err := row.Scan(&id) + err = row.Scan(&id) switch { case err == sql.ErrNoRows: @@ -249,6 +259,53 @@ func idForRow(row *sql.Row) (core.NoteID, error) { } } +func (d *NoteDAO) FindIdByHref(href string, allowPartialHref bool) (core.NoteID, error) { + ids, err := d.FindIdsByHref(href, allowPartialHref) + if len(ids) == 0 || err != nil { + return 0, err + } + return ids[0], nil +} + +func (d *NoteDAO) findIdsByHrefs(hrefs []string, allowPartialHrefs bool) ([]core.NoteID, error) { + ids := make([]core.NoteID, 0) + for _, href := range hrefs { + cids, err := d.FindIdsByHref(href, allowPartialHrefs) + if err != nil { + return ids, err + } + ids = append(ids, cids...) + } + return ids, nil +} + +func (d *NoteDAO) FindIdsByHref(href string, allowPartialHref bool) ([]core.NoteID, error) { + // Remove any anchor at the end of the HREF, since it's most likely + // matching a sub-section in the note. + href = strings.SplitN(href, "#", 2)[0] + + href = icu.EscapePattern(href) + + if allowPartialHref { + ids, err := d.findIdsByPathRegex("^(.*/)?[^/]*" + href + "[^/]*$") + if len(ids) > 0 || err != nil { + return ids, err + } + + ids, err = d.findIdsByPathRegex(".*" + href + ".*") + if len(ids) > 0 || err != nil { + return ids, err + } + } + + ids, err := d.findIdsByPathRegex(href + "[^/]*|" + href + "/.+") + if len(ids) > 0 || err != nil { + return ids, err + } + + return []core.NoteID{}, nil +} + func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error) { notes := make([]core.MinimalNote, 0) @@ -328,15 +385,16 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind } // Find the IDs for the mentioned paths. - ids, err := d.FindIdsByHrefs(opts.Mention, true /* allowPartialMatch */) + ids, err := d.findIdsByHrefs(opts.Mention, true /* allowPartialHrefs */) if err != nil { return opts, err } + if len(ids) == 0 { + return opts, fmt.Errorf("could not find notes at: " + strings.Join(opts.Mention, ", ")) + } // Exclude the mentioned notes from the results. - for _, id := range ids { - opts = opts.ExcludingID(id) - } + opts = opts.ExcludingIDs(ids) // Find their titles. titlesQuery := "SELECT title, metadata FROM notes WHERE id IN (" + joinNoteIDs(ids, ",") + ")" @@ -391,7 +449,7 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq maxDistance := 0 setupLinkFilter := func(hrefs []string, direction int, negate, recursive bool) error { - ids, err := d.FindIdsByHrefs(hrefs, true /* allowPartialMatch */) + ids, err := d.findIdsByHrefs(hrefs, true /* allowPartialHrefs */) if err != nil { return err } @@ -472,28 +530,20 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sq } } - if opts.IncludePaths != nil { - regexes := make([]string, 0) - for _, path := range opts.IncludePaths { - regexes = append(regexes, "n.path REGEXP ?") - if !opts.EnablePathRegexes { - path = pathRegex(path) - } - args = append(args, path) + if opts.IncludeHrefs != nil { + ids, err := d.findIdsByHrefs(opts.IncludeHrefs, opts.AllowPartialHrefs) + if err != nil { + return nil, err } - whereExprs = append(whereExprs, strings.Join(regexes, " OR ")) + opts = opts.IncludingIDs(ids) } - if opts.ExcludePaths != nil { - regexes := make([]string, 0) - for _, path := range opts.ExcludePaths { - regexes = append(regexes, "n.path NOT REGEXP ?") - if !opts.EnablePathRegexes { - path = pathRegex(path) - } - args = append(args, path) + if opts.ExcludeHrefs != nil { + ids, err := d.findIdsByHrefs(opts.ExcludeHrefs, opts.AllowPartialHrefs) + if err != nil { + return nil, err } - whereExprs = append(whereExprs, strings.Join(regexes, " AND ")) + opts = opts.ExcludingIDs(ids) } if opts.Tags != nil { @@ -545,15 +595,16 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s)) } if opts.MentionedBy != nil { - ids, err := d.FindIdsByHrefs(opts.MentionedBy, true /* allowPartialMatch */) + ids, err := d.findIdsByHrefs(opts.MentionedBy, true /* allowPartialHrefs */) if err != nil { return nil, err } + if len(ids) == 0 { + return nil, fmt.Errorf("could not find notes at: " + strings.Join(opts.MentionedBy, ", ")) + } // Exclude the mentioning notes from the results. - for _, id := range ids { - opts = opts.ExcludingID(id) - } + opts = opts.ExcludingIDs(ids) snippetCol = `snippet(nsrc.notes_fts, 2, '', '', '…', 20)` joinClauses = append(joinClauses, "JOIN notes_fts nsrc ON nsrc.rowid IN ("+joinNoteIDs(ids, ",")+") AND nsrc.notes_fts MATCH mention_query(n.title, n.metadata)") @@ -562,7 +613,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s)) if opts.LinkedBy != nil { filter := opts.LinkedBy maxDistance = filter.MaxDistance - err := setupLinkFilter(filter.Paths, -1, filter.Negate, filter.Recursive) + err := setupLinkFilter(filter.Hrefs, -1, filter.Negate, filter.Recursive) if err != nil { return nil, err } @@ -571,7 +622,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s)) if opts.LinkTo != nil { filter := opts.LinkTo maxDistance = filter.MaxDistance - err := setupLinkFilter(filter.Paths, 1, filter.Negate, filter.Recursive) + err := setupLinkFilter(filter.Hrefs, 1, filter.Negate, filter.Recursive) if err != nil { return nil, err } @@ -612,6 +663,10 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s)) args = append(args, opts.ModifiedEnd) } + if opts.IncludeIDs != nil { + whereExprs = append(whereExprs, "n.id IN ("+joinNoteIDs(opts.IncludeIDs, ",")+")") + } + if opts.ExcludeIDs != nil { whereExprs = append(whereExprs, "n.id NOT IN ("+joinNoteIDs(opts.ExcludeIDs, ",")+")") } @@ -793,20 +848,11 @@ func orderTerm(sorter core.NoteSorter) string { return "n.title" + order case core.NoteSortWordCount: return "n.word_count" + order - case core.NoteSortPathLength: - return "LENGTH(path)" + order default: panic(fmt.Sprintf("%v: unknown core.NoteSortField", sorter.Field)) } } -// pathRegex returns an ICU regex to match the files in the folder at given -// `path`, or any file having `path` for prefix. -func pathRegex(path string) string { - path = icu.EscapePattern(path) - return path + "[^/]*|" + path + "/.+" -} - // buildMentionQuery creates an FTS5 predicate to match the given note's title // (or aliases from the metadata) in the content of another note. // diff --git a/internal/adapter/sqlite/note_dao_test.go b/internal/adapter/sqlite/note_dao_test.go index 4911082..ce9e034 100644 --- a/internal/adapter/sqlite/note_dao_test.go +++ b/internal/adapter/sqlite/note_dao_test.go @@ -226,12 +226,72 @@ func TestNoteDAORemoveCascadeLinks(t *testing.T) { }) } +func TestNoteDAOFindIdsByHref(t *testing.T) { + test := func(href string, allowPartialHref bool, expected []core.NoteID) { + testNoteDAO(t, func(tx Transaction, dao *NoteDAO) { + actual, err := dao.FindIdsByHref(href, allowPartialHref) + assert.Nil(t, err) + assert.Equal(t, actual, expected) + }) + } + + test("test", false, []core.NoteID{}) + test("test", true, []core.NoteID{6, 5, 8}) + + // Filename takes precedence over the rest of the path. + // See https://github.com/mickael-menu/zk/issues/111 + test("ref", true, []core.NoteID{8}) +} + +func TestNoteDAOFindIncludingHrefs(t *testing.T) { + test := func(href string, allowPartialHref bool, expected []string) { + testNoteDAOFindPaths(t, + core.NoteFindOpts{ + IncludeHrefs: []string{href}, + AllowPartialHrefs: allowPartialHref, + }, + expected, + ) + } + + test("test", false, []string{}) + test("test", true, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"}) + + // Filename takes precedence over the rest of the path. + // See https://github.com/mickael-menu/zk/issues/111 + test("ref", true, []string{"ref/test/ref.md"}) +} + +func TestNoteDAOFindExcludingHrefs(t *testing.T) { + test := func(href string, allowPartialHref bool, expected []string) { + testNoteDAOFindPaths(t, + core.NoteFindOpts{ + ExcludeHrefs: []string{href}, + AllowPartialHrefs: allowPartialHref, + }, + expected, + ) + } + + test("test", false, []string{"ref/test/ref.md", "ref/test/b.md", + "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "log/2021-02-04.md", + "index.md", "log/2021-01-04.md"}) + test("test", true, []string{"f39c8.md", "log/2021-01-03.md", + "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) + + // Filename takes precedence over the rest of the path. + // See https://github.com/mickael-menu/zk/issues/111 + test("ref", true, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", + "log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) +} + func TestNoteDAOFindMinimalAll(t *testing.T) { testNoteDAO(t, func(tx Transaction, dao *NoteDAO) { notes, err := dao.FindMinimal(core.NoteFindOpts{}) assert.Nil(t, err) assert.Equal(t, notes, []core.MinimalNote{ + {ID: 8, Path: "ref/test/ref.md", Title: "", Metadata: map[string]interface{}{}}, {ID: 5, Path: "ref/test/b.md", Title: "A nested note", Metadata: map[string]interface{}{}}, {ID: 4, Path: "f39c8.md", Title: "An interesting note", Metadata: map[string]interface{}{}}, {ID: 6, Path: "ref/test/a.md", Title: "Another nested note", Metadata: map[string]interface{}{ @@ -272,13 +332,14 @@ func TestNoteDAOFindMinimalWithFilter(t *testing.T) { func TestNoteDAOFindAll(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{}, []string{ - "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", + "ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md", }) } func TestNoteDAOFindLimit(t *testing.T) { - testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 2}, []string{ + testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 3}, []string{ + "ref/test/ref.md", "ref/test/b.md", "f39c8.md", }) @@ -298,9 +359,9 @@ func TestNoteDAOFindTag(t *testing.T) { test([]string{"fiction | adventure | fantasy"}, []string{"ref/test/b.md", "f39c8.md", "log/2021-01-03.md"}) test([]string{"fiction | history", "adventure"}, []string{"ref/test/b.md", "log/2021-01-03.md"}) test([]string{"fiction", "unknown"}, []string{}) - test([]string{"-fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) - test([]string{"NOT fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) - test([]string{"NOTfiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) + test([]string{"-fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) + test([]string{"NOT fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) + test([]string{"NOTfiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}) } func TestNoteDAOFindMatch(t *testing.T) { @@ -434,7 +495,7 @@ func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) { func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - IncludePaths: []string{"log/2021-01-03.md"}, + IncludeHrefs: []string{"log/2021-01-03.md"}, }, []string{"log/2021-01-03.md"}, ) @@ -444,7 +505,7 @@ func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) { func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - IncludePaths: []string{"log/2021-01"}, + IncludeHrefs: []string{"log/2021-01"}, }, []string{"log/2021-01-03.md", "log/2021-01-04.md"}, ) @@ -454,13 +515,15 @@ func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) { func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - IncludePaths: []string{"lo"}, + IncludeHrefs: []string{"lo"}, + AllowPartialHrefs: false, }, []string{}, ) testNoteDAOFindPaths(t, core.NoteFindOpts{ - IncludePaths: []string{"log"}, + IncludeHrefs: []string{"log"}, + AllowPartialHrefs: false, }, []string{"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md"}, ) @@ -470,25 +533,25 @@ func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) { func TestNoteDAOFindInMultiplePaths(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - IncludePaths: []string{"ref", "index.md"}, + IncludeHrefs: []string{"ref", "index.md"}, }, - []string{"ref/test/b.md", "ref/test/a.md", "index.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"}, ) } func TestNoteDAOFindExcludingPath(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - ExcludePaths: []string{"log"}, + ExcludeHrefs: []string{"log"}, }, - []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"}, ) } func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - ExcludePaths: []string{"ref", "log/2021-01"}, + ExcludeHrefs: []string{"ref", "log/2021-01"}, }, []string{"f39c8.md", "log/2021-02-04.md", "index.md"}, ) @@ -562,7 +625,7 @@ func TestNoteDAOFindUnlinkedMentions(t *testing.T) { core.NoteFindOpts{ Mention: []string{"log/2021-01-03.md", "index.md"}, LinkTo: &core.LinkFilter{ - Paths: []string{"log/2021-01-03.md", "index.md"}, + Hrefs: []string{"log/2021-01-03.md", "index.md"}, Negate: true, }, }, @@ -626,7 +689,7 @@ func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) { core.NoteFindOpts{ MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"}, LinkedBy: &core.LinkFilter{ - Paths: []string{"ref/test/b.md", "log/2021-01-04.md"}, + Hrefs: []string{"ref/test/b.md", "log/2021-01-04.md"}, Negate: true, }, }, @@ -638,7 +701,7 @@ func TestNoteDAOFindLinkedBy(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkedBy: &core.LinkFilter{ - Paths: []string{"f39c8.md", "log/2021-01-03"}, + Hrefs: []string{"f39c8.md", "log/2021-01-03"}, Negate: false, Recursive: false, }, @@ -651,7 +714,7 @@ func TestNoteDAOFindLinkedByRecursive(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkedBy: &core.LinkFilter{ - Paths: []string{"log/2021-01-04.md"}, + Hrefs: []string{"log/2021-01-04.md"}, Negate: false, Recursive: true, }, @@ -664,7 +727,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkedBy: &core.LinkFilter{ - Paths: []string{"log/2021-01-04.md"}, + Hrefs: []string{"log/2021-01-04.md"}, Negate: false, Recursive: true, MaxDistance: 2, @@ -677,7 +740,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) { func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) { testNoteDAOFind(t, core.NoteFindOpts{ - LinkedBy: &core.LinkFilter{Paths: []string{"f39c8.md"}}, + LinkedBy: &core.LinkFilter{Hrefs: []string{"f39c8.md"}}, }, []core.ContextualNote{ { @@ -733,12 +796,12 @@ func TestNoteDAOFindNotLinkedBy(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkedBy: &core.LinkFilter{ - Paths: []string{"f39c8.md", "log/2021-01-03"}, + Hrefs: []string{"f39c8.md", "log/2021-01-03"}, Negate: true, Recursive: false, }, }, - []string{"ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"}, ) } @@ -746,7 +809,7 @@ func TestNoteDAOFindLinkTo(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkTo: &core.LinkFilter{ - Paths: []string{"log/2021-01-04", "ref/test/a.md"}, + Hrefs: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: false, Recursive: false, }, @@ -759,7 +822,7 @@ func TestNoteDAOFindLinkToRecursive(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkTo: &core.LinkFilter{ - Paths: []string{"log/2021-01-04.md"}, + Hrefs: []string{"log/2021-01-04.md"}, Negate: false, Recursive: true, }, @@ -772,7 +835,7 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ LinkTo: &core.LinkFilter{ - Paths: []string{"log/2021-01-04.md"}, + Hrefs: []string{"log/2021-01-04.md"}, Negate: false, Recursive: true, MaxDistance: 2, @@ -785,9 +848,9 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) { func TestNoteDAOFindNotLinkTo(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{ - LinkTo: &core.LinkFilter{Paths: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true}, + LinkTo: &core.LinkFilter{Hrefs: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true}, }, - []string{"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"}, ) } @@ -810,7 +873,7 @@ func TestNoteDAOFindRelated(t *testing.T) { func TestNoteDAOFindOrphan(t *testing.T) { testNoteDAOFindPaths(t, core.NoteFindOpts{Orphan: true}, - []string{"ref/test/b.md", "log/2021-02-04.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "log/2021-02-04.md"}, ) } @@ -832,7 +895,7 @@ func TestNoteDAOFindCreatedBefore(t *testing.T) { core.NoteFindOpts{ CreatedEnd: &end, }, - []string{"ref/test/b.md", "ref/test/a.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"}, ) } @@ -864,7 +927,7 @@ func TestNoteDAOFindModifiedBefore(t *testing.T) { core.NoteFindOpts{ ModifiedEnd: &end, }, - []string{"ref/test/b.md", "ref/test/a.md", "index.md"}, + []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"}, ) } @@ -880,55 +943,55 @@ func TestNoteDAOFindModifiedAfter(t *testing.T) { func TestNoteDAOFindSortCreated(t *testing.T) { testNoteDAOFindSort(t, core.NoteSortCreated, true, []string{ - "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", + "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", "log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md", }) testNoteDAOFindSort(t, core.NoteSortCreated, false, []string{ "log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md", - "f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md", + "f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", }) } func TestNoteDAOFindSortModified(t *testing.T) { testNoteDAOFindSort(t, core.NoteSortModified, true, []string{ - "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", + "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md", "log/2021-02-04.md", "log/2021-01-03.md", "log/2021-01-04.md", }) testNoteDAOFindSort(t, core.NoteSortModified, false, []string{ "log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md", - "f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md", + "f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", }) } func TestNoteDAOFindSortPath(t *testing.T) { testNoteDAOFindSort(t, core.NoteSortPath, true, []string{ "f39c8.md", "index.md", "log/2021-01-03.md", "log/2021-01-04.md", - "log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md", + "log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md", "ref/test/ref.md", }) testNoteDAOFindSort(t, core.NoteSortPath, false, []string{ - "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", + "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md", "index.md", "f39c8.md", }) } func TestNoteDAOFindSortTitle(t *testing.T) { testNoteDAOFindSort(t, core.NoteSortTitle, true, []string{ - "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", + "ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md", }) testNoteDAOFindSort(t, core.NoteSortTitle, false, []string{ "log/2021-01-04.md", "index.md", "log/2021-02-04.md", - "log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md", + "log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md", "ref/test/ref.md", }) } func TestNoteDAOFindSortWordCount(t *testing.T) { testNoteDAOFindSort(t, core.NoteSortWordCount, true, []string{ "log/2021-01-03.md", "log/2021-02-04.md", "index.md", - "log/2021-01-04.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md", + "log/2021-01-04.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md", }) testNoteDAOFindSort(t, core.NoteSortWordCount, false, []string{ - "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", + "ref/test/b.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md", "log/2021-01-03.md", }) } diff --git a/internal/adapter/sqlite/testdata/default/notes.yml b/internal/adapter/sqlite/testdata/default/notes.yml index 3cfb5cf..970b07f 100644 --- a/internal/adapter/sqlite/testdata/default/notes.yml +++ b/internal/adapter/sqlite/testdata/default/notes.yml @@ -88,3 +88,16 @@ created: "2020-11-29T08:20:18Z" modified: "2020-11-10T08:20:18Z" metadata: "{}" + +- id: 8 + path: "ref/test/ref.md" + sortable_path: "ref/ref.md" + title: "" + lead: "" + body: "" + raw_content: "" + word_count: 5 + checksum: "ientrs" + created: "2019-11-20T20:32:56Z" + modified: "2019-11-20T20:34:06Z" + metadata: '{}' diff --git a/internal/cli/filtering.go b/internal/cli/filtering.go index 0596b24..351c4b5 100644 --- a/internal/cli/filtering.go +++ b/internal/cli/filtering.go @@ -142,11 +142,11 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts, opts.ExactMatch = f.ExactMatch if paths, ok := relPaths(notebook, f.Path); ok { - opts.IncludePaths = paths + opts.IncludeHrefs = paths } if paths, ok := relPaths(notebook, f.Exclude); ok { - opts.ExcludePaths = paths + opts.ExcludeHrefs = paths } if len(f.Tag) > 0 { @@ -163,28 +163,28 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts, if paths, ok := relPaths(notebook, f.LinkedBy); ok { opts.LinkedBy = &core.LinkFilter{ - Paths: paths, + Hrefs: paths, Negate: false, Recursive: f.Recursive, MaxDistance: f.MaxDistance, } } else if paths, ok := relPaths(notebook, f.NoLinkedBy); ok { opts.LinkedBy = &core.LinkFilter{ - Paths: paths, + Hrefs: paths, Negate: true, } } if paths, ok := relPaths(notebook, f.LinkTo); ok { opts.LinkTo = &core.LinkFilter{ - Paths: paths, + Hrefs: paths, Negate: false, Recursive: f.Recursive, MaxDistance: f.MaxDistance, } } else if paths, ok := relPaths(notebook, f.NoLinkTo); ok { opts.LinkTo = &core.LinkFilter{ - Paths: paths, + Hrefs: paths, Negate: true, } } diff --git a/internal/core/note_find.go b/internal/core/note_find.go index 3bef2a7..832425b 100644 --- a/internal/core/note_find.go +++ b/internal/core/note_find.go @@ -6,7 +6,6 @@ import ( "time" "unicode/utf8" - "github.com/mickael-menu/zk/internal/util/icu" "github.com/mickael-menu/zk/internal/util/opt" ) @@ -16,12 +15,15 @@ type NoteFindOpts struct { Match opt.String // Search for exact occurrences of the Match string. ExactMatch bool - // Filter by note paths. - IncludePaths []string - // Filter excluding notes at the given paths. - ExcludePaths []string - // Indicates whether IncludePaths and ExcludePaths are using regexes. - EnablePathRegexes bool + // Filter by note hrefs. + IncludeHrefs []string + // Filter excluding notes at the given hrefs. + ExcludeHrefs []string + // Indicates whether href options can match any portion of a path. + // This is used for wiki links. + AllowPartialHrefs bool + // Filter including notes with the given IDs. + IncludeIDs []NoteID // Filter excluding notes with the given IDs. ExcludeIDs []NoteID // Filter by tags found in the notes. @@ -34,7 +36,7 @@ type NoteFindOpts struct { LinkedBy *LinkFilter // Filter to select notes linking to another one. LinkTo *LinkFilter - // Filter to select notes which could might be related to the given notes paths. + // Filter to select notes which could might be related to the given notes hrefs. Related []string // Filter to select notes having no other notes linking to them. Orphan bool @@ -52,42 +54,31 @@ type NoteFindOpts struct { Sorters []NoteSorter } -// NewNoteFindOptsByHref creates a new set of filtering options to find a note -// from a link href. -// If allowPartialMatch is true, the href can match any unique sub portion of a note path. -func NewNoteFindOptsByHref(href string, allowPartialMatch bool) NoteFindOpts { - // Remove any anchor at the end of the HREF, since it's most likely - // matching a sub-section in the note. - href = strings.SplitN(href, "#", 2)[0] - - if allowPartialMatch { - href = "(.*)" + icu.EscapePattern(href) + "(.*)" +// IncludingIDs creates a new FinderOpts after adding the given IDs to the list +// of excluded note IDs. +func (o NoteFindOpts) IncludingIDs(ids []NoteID) NoteFindOpts { + if o.IncludeIDs == nil { + o.IncludeIDs = []NoteID{} } - return NoteFindOpts{ - IncludePaths: []string{href}, - EnablePathRegexes: allowPartialMatch, - // To find the best match possible, we sort by path length. - // See https://github.com/mickael-menu/zk/issues/23 - Sorters: []NoteSorter{{Field: NoteSortPathLength, Ascending: true}}, - Limit: 1, - } + o.IncludeIDs = append(o.IncludeIDs, ids...) + return o } -// ExcludingID creates a new FinderOpts after adding the given ID to the list +// ExcludingIDs creates a new FinderOpts after adding the given IDs to the list // of excluded note IDs. -func (o NoteFindOpts) ExcludingID(id NoteID) NoteFindOpts { +func (o NoteFindOpts) ExcludingIDs(ids []NoteID) NoteFindOpts { if o.ExcludeIDs == nil { o.ExcludeIDs = []NoteID{} } - o.ExcludeIDs = append(o.ExcludeIDs, id) + o.ExcludeIDs = append(o.ExcludeIDs, ids...) return o } // LinkFilter is a note filter used to select notes linking to other ones. type LinkFilter struct { - Paths []string + Hrefs []string Negate bool Recursive bool MaxDistance int @@ -115,10 +106,6 @@ const ( NoteSortTitle // Sort by the number of words in the note bodies. NoteSortWordCount - // Sort by the length of the note path. - // This is not accessible to the user but used for technical reasons, to - // find the best match when searching a path prefix. - NoteSortPathLength ) // NoteSortersFromStrings returns a list of NoteSorter from their string diff --git a/internal/core/notebook.go b/internal/core/notebook.go index 35ecdae..0308539 100644 --- a/internal/core/notebook.go +++ b/internal/core/notebook.go @@ -224,9 +224,12 @@ func (n *Notebook) FindMinimalNote(opts NoteFindOpts) (*MinimalNote, error) { } // FindByHref retrieves the first note matching the given link href. -// If allowPartialMatch is true, the href can match any unique sub portion of a note path. -func (n *Notebook) FindByHref(href string, allowPartialMatch bool) (*MinimalNote, error) { - return n.FindMinimalNote(NewNoteFindOptsByHref(href, allowPartialMatch)) +// If allowPartialHref is true, the href can match any unique sub portion of a note path. +func (n *Notebook) FindByHref(href string, allowPartialHref bool) (*MinimalNote, error) { + return n.FindMinimalNote(NoteFindOpts{ + IncludeHrefs: []string{href}, + AllowPartialHrefs: allowPartialHref, + }) } // FindMatching retrieves the first note matching the given search terms.