diff --git a/CHANGELOG.md b/CHANGELOG.md index 599cecd..e82679c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## Unreleased +### Added + +* New LSP commands: + * [`zk.list`](docs/editors-integration.md#zklist) to search for notes. + * [`zk.tag.list`](docs/editors-integration.md#zktaglist) to retrieve the list of tags. + ### Fixed * [#111](https://github.com/mickael-menu/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links. diff --git a/docs/editors-integration.md b/docs/editors-integration.md index 3e26c4a..f10632a 100644 --- a/docs/editors-integration.md +++ b/docs/editors-integration.md @@ -150,17 +150,17 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl 1. A path to any file or directory in the notebook, to locate it. 2.
(Optional) A dictionary of additional options (click to expand) - | Key | Type | Description | - |------------------------|------------|-------------------------------------------------------------------------------------------| - | `title` | string | Title of the new note | - | `content` | string | Initial content of the note | - | `dir` | string | Parent directory, relative to the root of the notebook | - | `group` | string | [Note configuration group](config-group.md) | - | `template` | string | [Custom template used to render the note](template-creation.md) | - | `extra` | dictionary | A dictionary of extra variables to expand in the template | - | `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" | - | `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) | - | `insertLinkAtLocation` | location | A location in another note where a link to the new note will be inserted | + | Key | Type | Description | + |------------------------|----------------------|-------------------------------------------------------------------------------------------| + | `title` | string | Title of the new note | + | `content` | string | Initial content of the note | + | `dir` | string | Parent directory, relative to the root of the notebook | + | `group` | string | [Note configuration group](config-group.md) | + | `template` | string | [Custom template used to render the note](template-creation.md) | + | `extra` | dictionary | A dictionary of extra variables to expand in the template | + | `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" | + | `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) | + | `insertLinkAtLocation` | location1 | A location in another note where a link to the new note will be inserted | The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example: @@ -176,3 +176,58 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl
`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file. + +#### `zk.list` + +This LSP command calls `zk list` to search a notebook. It takes two arguments: + +1. A path to any file or directory in the notebook, to locate it. +2.
A dictionary of additional options (click to expand) + + | Key | Type | Required? | Description | + |------------------|--------------|-----------|-------------------------------------------------------------------------| + | `select` | string array | Yes | List of note fields to return1 | + | `hrefs` | string array | No | Find notes matching the given path, including its descendants | + | `limit` | integer | No | Limit the number of notes found | + | `match` | string | No | Terms to search for in the notes | + | `exactMatch` | boolean | No | Search for exact occurrences of the `match` argument (case insensitive) | + | `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants | + | `tags` | string array | No | Find notes tagged with the given tags | + | `mention` | string array | No | Find notes mentioning the title of the given ones | + | `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones | + | `linkTo` | string array | No | Find notes which are linking to the given ones | + | `linkedBy` | string array | No | Find notes which are linked by the given ones | + | `orphan` | boolean | No | Find notes which are not linked by any other note | + | `related` | string array | No | Find notes which might be related to the given ones | + | `maxDistance` | integer | No | Maximum distance between two linked notes | + | `recursive` | boolean | No | Follow links recursively | + | `created` | string | No | Find notes created on the given date | + | `createdBefore` | string | No | Find notes created before the given date | + | `createdAfter` | string | No | Find notes created after the given date | + | `modified` | string | No | Find notes modified on the given date | + | `modifiedBefore` | string | No | Find notes modified before the given date | + | `modifiedAfter` | string | No | Find notes modified after the given date | + | `sort` | string array | No | Order the notes by the given criterion | + + 1. As the output of this command might be very verbose and put a heavy load on the LSP client, you need to explicitly set which note fields you want to receive with the `select` option. The following fields are available: `filename`, `filenameStem`, `path`, `absPath`, `title`, `lead`, `body`, `snippets`, `rawContent`, `wordCount`, `tags`, `metadata`, `created`, `modified` and `checksum`. + +
+ +`zk.list` returns the found notes as a JSON array. + +#### `zk.tag.list` + +This LSP command calls `zk tag list` to return the list of tags in a notebook. It takes two arguments: + +1. A path to any file or directory in the notebook, to locate it. +2.
(Optional) A dictionary of additional options (click to expand) + + | Key | Type | Required? | Description | + |--------|--------------|-----------|--------------------------------------------------| + | `sort` | string array | No | Order the tags by the given criteria1 | + + 1. The available sort criteria are `name` and `note-count`. You can change the order by appending `-` or `+` to the criterion. + +
+ +`zk.tag.list` returns the tags as a JSON array. diff --git a/internal/adapter/lsp/cmd_index.go b/internal/adapter/lsp/cmd_index.go new file mode 100644 index 0000000..ccd1df6 --- /dev/null +++ b/internal/adapter/lsp/cmd_index.go @@ -0,0 +1,27 @@ +package lsp + +import ( + "fmt" + + "github.com/mickael-menu/zk/internal/core" +) + +const cmdIndex = "zk.index" + +func executeCommandIndex(notebook *core.Notebook, args []interface{}) (interface{}, error) { + opts := core.NoteIndexOpts{} + if len(args) == 2 { + options, ok := args[1].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("zk.index expects a dictionary of options as second argument, got: %v", args[1]) + } + if forceOption, ok := options["force"]; ok { + opts.Force = toBool(forceOption) + } + if verboseOption, ok := options["verbose"]; ok { + opts.Verbose = toBool(verboseOption) + } + } + + return notebook.Index(opts) +} diff --git a/internal/adapter/lsp/cmd_list.go b/internal/adapter/lsp/cmd_list.go new file mode 100644 index 0000000..437a269 --- /dev/null +++ b/internal/adapter/lsp/cmd_list.go @@ -0,0 +1,162 @@ +package lsp + +import ( + "fmt" + "path/filepath" + "time" + + "github.com/mickael-menu/zk/internal/cli" + "github.com/mickael-menu/zk/internal/core" + "github.com/mickael-menu/zk/internal/util" + "github.com/mickael-menu/zk/internal/util/errors" + strutil "github.com/mickael-menu/zk/internal/util/strings" +) + +const cmdList = "zk.list" + +type cmdListOpts struct { + Select []string `json:"select"` + cli.Filtering +} + +func executeCommandList(logger util.Logger, notebook *core.Notebook, args []interface{}) (interface{}, error) { + var opts cmdListOpts + if len(args) > 1 { + arg, ok := args[1].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdTagList, args[1]) + } + err := unmarshalJSON(arg, &opts) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdTagList, arg) + } + } + + if len(opts.Select) == 0 { + return nil, fmt.Errorf("%s expects a `select` option with the list of fields to return", cmdTagList) + } + var selection = newListSelection(opts.Select) + + findOpts, err := opts.NewNoteFindOpts(notebook) + if err != nil { + return nil, err + } + + notes, err := notebook.FindNotes(findOpts) + if err != nil { + return nil, err + } + + listNotes := []listNote{} + for _, note := range notes { + listNotes = append(listNotes, newListNote(note, selection, notebook.Path)) + } + + return listNotes, nil +} + +type listSelection struct { + Filename bool + FilenameStem bool + Path bool + AbsPath bool + Title bool + Lead bool + Body bool + Snippets bool + RawContent bool + WordCount bool + Tags bool + Metadata bool + Created bool + Modified bool + Checksum bool +} + +func newListSelection(fields []string) listSelection { + return listSelection{ + Filename: strutil.Contains(fields, "filename"), + FilenameStem: strutil.Contains(fields, "filenameStem"), + Path: strutil.Contains(fields, "path"), + AbsPath: strutil.Contains(fields, "absPath"), + Title: strutil.Contains(fields, "title"), + Lead: strutil.Contains(fields, "lead"), + Body: strutil.Contains(fields, "body"), + Snippets: strutil.Contains(fields, "snippets"), + RawContent: strutil.Contains(fields, "rawContent"), + WordCount: strutil.Contains(fields, "wordCount"), + Tags: strutil.Contains(fields, "tags"), + Metadata: strutil.Contains(fields, "metadata"), + Created: strutil.Contains(fields, "created"), + Modified: strutil.Contains(fields, "modified"), + Checksum: strutil.Contains(fields, "checksum"), + } +} + +func newListNote(note core.ContextualNote, selection listSelection, basePath string) listNote { + var res listNote + if selection.Filename { + res.Filename = note.Filename() + } + if selection.FilenameStem { + res.FilenameStem = note.FilenameStem() + } + if selection.Path { + res.Path = note.Path + } + if selection.AbsPath { + res.AbsPath = filepath.Join(basePath, note.Path) + } + if selection.Title { + res.Title = note.Title + } + if selection.Lead { + res.Lead = note.Lead + } + if selection.Body { + res.Body = note.Body + } + if selection.Snippets { + res.Snippets = note.Snippets + } + if selection.RawContent { + res.RawContent = note.RawContent + } + if selection.WordCount { + res.WordCount = note.WordCount + } + if selection.Tags { + res.Tags = note.Tags + } + if selection.Metadata { + res.Metadata = note.Metadata + } + if selection.Created { + res.Created = ¬e.Created + } + if selection.Modified { + res.Modified = ¬e.Modified + } + if selection.Checksum { + res.Checksum = note.Checksum + } + return res +} + +type listNote struct { + Filename string `json:"filename,omitempty"` + FilenameStem string `json:"filenameStem,omitempty"` + Path string `json:"path,omitempty"` + AbsPath string `json:"absPath,omitempty"` + Title string `json:"title,omitempty"` + Lead string `json:"lead,omitempty"` + Body string `json:"body,omitempty"` + Snippets []string `json:"snippets,omitempty"` + RawContent string `json:"rawContent,omitempty"` + WordCount int `json:"wordCount,omitempty"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Created *time.Time `json:"created,omitempty"` + Modified *time.Time `json:"modified,omitempty"` + Checksum string `json:"checksum,omitempty"` +} diff --git a/internal/adapter/lsp/cmd_new.go b/internal/adapter/lsp/cmd_new.go new file mode 100644 index 0000000..8cf5fa1 --- /dev/null +++ b/internal/adapter/lsp/cmd_new.go @@ -0,0 +1,111 @@ +package lsp + +import ( + "fmt" + "path/filepath" + + "github.com/mickael-menu/zk/internal/core" + dateutil "github.com/mickael-menu/zk/internal/util/date" + "github.com/mickael-menu/zk/internal/util/errors" + "github.com/mickael-menu/zk/internal/util/opt" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +const cmdNew = "zk.new" + +type cmdNewOpts struct { + Title string `json:"title"` + Content string `json:"content"` + Dir string `json:"dir"` + Group string `json:"group"` + Template string `json:"template"` + Extra map[string]string `json:"extra"` + Date string `json:"date"` + Edit jsonBoolean `json:"edit"` + InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation"` +} + +func executeCommandNew(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) { + var opts cmdNewOpts + if len(args) > 1 { + arg, ok := args[1].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdNew, args[1]) + } + err := unmarshalJSON(arg, &opts) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdNew, arg) + } + } + + date, err := dateutil.TimeFromNatural(opts.Date) + if err != nil { + return nil, errors.Wrapf(err, "%s, failed to parse the `date` option", opts.Date) + } + + note, err := notebook.NewNote(core.NewNoteOpts{ + Title: opt.NewNotEmptyString(opts.Title), + Content: opts.Content, + Directory: opt.NewNotEmptyString(opts.Dir), + Group: opt.NewNotEmptyString(opts.Group), + Template: opt.NewNotEmptyString(opts.Template), + Extra: opts.Extra, + Date: date, + }) + if err != nil { + var noteExists core.ErrNoteExists + if !errors.As(err, ¬eExists) { + return nil, err + } + note, err = notebook.FindNote(core.NoteFindOpts{ + IncludeHrefs: []string{noteExists.Name}, + }) + if err != nil { + return nil, err + } + } + if note == nil { + return nil, errors.New("zk.new could not generate a new note") + } + + if opts.InsertLinkAtLocation != nil { + doc, ok := documents.Get(opts.InsertLinkAtLocation.URI) + if !ok { + return nil, fmt.Errorf("can't insert link in %s", opts.InsertLinkAtLocation.URI) + } + linkFormatter, err := notebook.NewLinkFormatter() + if err != nil { + return nil, err + } + + currentDir := filepath.Dir(doc.Path) + linkFormatterContext, err := core.NewLinkFormatterContext(note.AsMinimalNote(), notebook.Path, currentDir) + if err != nil { + return nil, err + } + + link, err := linkFormatter(linkFormatterContext) + if err != nil { + return nil, err + } + + go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{ + Edit: protocol.WorkspaceEdit{ + Changes: map[string][]protocol.TextEdit{ + opts.InsertLinkAtLocation.URI: {{Range: opts.InsertLinkAtLocation.Range, NewText: link}}, + }, + }, + }, nil) + } + + absPath := filepath.Join(notebook.Path, note.Path) + if opts.Edit { + go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{ + URI: pathToURI(absPath), + TakeFocus: boolPtr(true), + }, nil) + } + + return map[string]interface{}{"path": absPath}, nil +} diff --git a/internal/adapter/lsp/cmd_tag.go b/internal/adapter/lsp/cmd_tag.go new file mode 100644 index 0000000..31e0fff --- /dev/null +++ b/internal/adapter/lsp/cmd_tag.go @@ -0,0 +1,39 @@ +package lsp + +import ( + "fmt" + + "github.com/mickael-menu/zk/internal/core" + "github.com/mickael-menu/zk/internal/util" + "github.com/mickael-menu/zk/internal/util/errors" +) + +const cmdTagList = "zk.tag.list" + +type cmdTagListOpts struct { + Sort []string `json:"sort"` +} + +func executeCommandTagList(logger util.Logger, notebook *core.Notebook, args []interface{}) (interface{}, error) { + var opts cmdTagListOpts + if len(args) > 1 { + arg, ok := args[1].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdTagList, args[1]) + } + err := unmarshalJSON(arg, &opts) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdTagList, arg) + } + } + + var sorters []core.CollectionSorter + var err error + if opts.Sort != nil { + sorters, err = core.CollectionSortersFromStrings(opts.Sort) + if err != nil { + return nil, err + } + } + return notebook.FindCollections(core.CollectionKindTag, sorters) +} diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go index 75ee984..f3c83f6 100644 --- a/internal/adapter/lsp/server.go +++ b/internal/adapter/lsp/server.go @@ -10,7 +10,6 @@ import ( "github.com/mickael-menu/zk/internal/core" "github.com/mickael-menu/zk/internal/util" - dateutil "github.com/mickael-menu/zk/internal/util/date" "github.com/mickael-menu/zk/internal/util/errors" "github.com/mickael-menu/zk/internal/util/opt" strutil "github.com/mickael-menu/zk/internal/util/strings" @@ -100,6 +99,8 @@ func NewServer(opts ServerOpts) *Server { Commands: []string{ cmdIndex, cmdNew, + cmdList, + cmdTagList, }, } capabilities.CompletionProvider = &protocol.CompletionOptions{ @@ -337,11 +338,49 @@ func NewServer(opts ServerOpts) *Server { } handler.WorkspaceExecuteCommand = func(context *glsp.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { + + openNotebook := func() (*core.Notebook, error) { + args := params.Arguments + if len(args) == 0 { + return nil, fmt.Errorf("%s expects a notebook path as first argument", params.Command) + } + path, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("%s expects a notebook path as first argument, got: %v", params.Command, args[0]) + } + + return server.notebooks.Open(path) + } + switch params.Command { case cmdIndex: - return server.executeCommandIndex(params.Arguments) + nb, err := openNotebook() + if err != nil { + return nil, err + } + return executeCommandIndex(nb, params.Arguments) + case cmdNew: - return server.executeCommandNew(context, params.Arguments) + nb, err := openNotebook() + if err != nil { + return nil, err + } + return executeCommandNew(nb, server.documents, context, params.Arguments) + + case cmdList: + nb, err := openNotebook() + if err != nil { + return nil, err + } + return executeCommandList(server.logger, nb, params.Arguments) + + case cmdTagList: + nb, err := openNotebook() + if err != nil { + return nil, err + } + return executeCommandTagList(server.logger, nb, params.Arguments) + default: return nil, fmt.Errorf("unknown zk LSP command: %s", params.Command) } @@ -474,150 +513,6 @@ func (s *Server) Run() error { return errors.Wrap(s.server.RunStdio(), "lsp") } -const cmdIndex = "zk.index" - -func (s *Server) executeCommandIndex(args []interface{}) (interface{}, error) { - if len(args) == 0 { - return nil, fmt.Errorf("zk.index expects a notebook path as first argument") - } - path, ok := args[0].(string) - if !ok { - return nil, fmt.Errorf("zk.index expects a notebook path as first argument, got: %v", args[0]) - } - - opts := core.NoteIndexOpts{} - if len(args) == 2 { - options, ok := args[1].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("zk.index expects a dictionary of options as second argument, got: %v", args[1]) - } - if forceOption, ok := options["force"]; ok { - opts.Force = toBool(forceOption) - } - if verboseOption, ok := options["verbose"]; ok { - opts.Verbose = toBool(verboseOption) - } - } - - notebook, err := s.notebooks.Open(path) - if err != nil { - return nil, err - } - - return notebook.Index(opts) -} - -const cmdNew = "zk.new" - -type cmdNewOpts struct { - Title string `json:"title,omitempty"` - Content string `json:"content,omitempty"` - Dir string `json:"dir,omitempty"` - Group string `json:"group,omitempty"` - Template string `json:"template,omitempty"` - Extra map[string]string `json:"extra,omitempty"` - Date string `json:"date,omitempty"` - Edit jsonBoolean `json:"edit,omitempty"` - InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation,omitempty"` -} - -func (s *Server) executeCommandNew(context *glsp.Context, args []interface{}) (interface{}, error) { - if len(args) == 0 { - return nil, fmt.Errorf("zk.index expects a notebook path as first argument") - } - wd, ok := args[0].(string) - if !ok { - return nil, fmt.Errorf("zk.index expects a notebook path as first argument, got: %v", args[0]) - } - - var opts cmdNewOpts - if len(args) > 1 { - arg, ok := args[1].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("zk.new expects a dictionary of options as second argument, got: %v", args[1]) - } - err := unmarshalJSON(arg, &opts) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse zk.new args, got: %v", arg) - } - } - - notebook, err := s.notebooks.Open(wd) - if err != nil { - return nil, err - } - - date, err := dateutil.TimeFromNatural(opts.Date) - if err != nil { - return nil, errors.Wrapf(err, "%s, failed to parse the `date` option", opts.Date) - } - - note, err := notebook.NewNote(core.NewNoteOpts{ - Title: opt.NewNotEmptyString(opts.Title), - Content: opts.Content, - Directory: opt.NewNotEmptyString(opts.Dir), - Group: opt.NewNotEmptyString(opts.Group), - Template: opt.NewNotEmptyString(opts.Template), - Extra: opts.Extra, - Date: date, - }) - if err != nil { - var noteExists core.ErrNoteExists - if !errors.As(err, ¬eExists) { - return nil, err - } - note, err = notebook.FindNote(core.NoteFindOpts{ - IncludeHrefs: []string{noteExists.Name}, - }) - if err != nil { - return nil, err - } - } - if note == nil { - return nil, errors.New("zk.new could not generate a new note") - } - - if opts.InsertLinkAtLocation != nil { - doc, ok := s.documents.Get(opts.InsertLinkAtLocation.URI) - if !ok { - return nil, fmt.Errorf("can't insert link in %s", opts.InsertLinkAtLocation.URI) - } - linkFormatter, err := notebook.NewLinkFormatter() - if err != nil { - return nil, err - } - - currentDir := filepath.Dir(doc.Path) - linkFormatterContext, err := core.NewLinkFormatterContext(note.AsMinimalNote(), notebook.Path, currentDir) - if err != nil { - return nil, err - } - - link, err := linkFormatter(linkFormatterContext) - if err != nil { - return nil, err - } - - go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{ - Edit: protocol.WorkspaceEdit{ - Changes: map[string][]protocol.TextEdit{ - opts.InsertLinkAtLocation.URI: {{Range: opts.InsertLinkAtLocation.Range, NewText: link}}, - }, - }, - }, nil) - } - - absPath := filepath.Join(notebook.Path, note.Path) - if opts.Edit { - go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{ - URI: pathToURI(absPath), - TakeFocus: boolPtr(true), - }, nil) - } - - return map[string]interface{}{"path": absPath}, nil -} - func (s *Server) notebookOf(doc *document) (*core.Notebook, error) { return s.notebooks.Open(doc.Path) } diff --git a/internal/adapter/sqlite/collection_dao.go b/internal/adapter/sqlite/collection_dao.go index 7d5869b..76eb5a3 100644 --- a/internal/adapter/sqlite/collection_dao.go +++ b/internal/adapter/sqlite/collection_dao.go @@ -79,7 +79,7 @@ func (d *CollectionDAO) FindOrCreate(kind core.CollectionKind, name string) (cor func (d *CollectionDAO) FindAll(kind core.CollectionKind, sorters []core.CollectionSorter) ([]core.Collection, error) { query := ` - SELECT c.name, COUNT(nc.id) as count + SELECT c.id, c.name, COUNT(nc.id) as count FROM collections c INNER JOIN notes_collections nc ON nc.collection_id = c.id WHERE kind = ? @@ -104,14 +104,16 @@ func (d *CollectionDAO) FindAll(kind core.CollectionKind, sorters []core.Collect collections := []core.Collection{} for rows.Next() { + var id sql.NullInt64 var name string var count int - err := rows.Scan(&name, &count) + err := rows.Scan(&id, &name, &count) if err != nil { return collections, err } collections = append(collections, core.Collection{ + ID: core.CollectionID(id.Int64), Kind: kind, Name: name, NoteCount: count, diff --git a/internal/adapter/sqlite/collection_dao_test.go b/internal/adapter/sqlite/collection_dao_test.go index f3e38f6..1e6a0ce 100644 --- a/internal/adapter/sqlite/collection_dao_test.go +++ b/internal/adapter/sqlite/collection_dao_test.go @@ -43,11 +43,11 @@ func TestCollectionDaoFindAll(t *testing.T) { cs, err = dao.FindAll("tag", nil) assert.Nil(t, err) assert.Equal(t, cs, []core.Collection{ - {Kind: "tag", Name: "adventure", NoteCount: 2}, - {Kind: "tag", Name: "fantasy", NoteCount: 1}, - {Kind: "tag", Name: "fiction", NoteCount: 1}, - {Kind: "tag", Name: "history", NoteCount: 1}, - {Kind: "tag", Name: "science", NoteCount: 3}, + {ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2}, + {ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1}, + {ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1}, + {ID: 5, Kind: "tag", Name: "history", NoteCount: 1}, + {ID: 7, Kind: "tag", Name: "science", NoteCount: 3}, }) }) } @@ -59,11 +59,11 @@ func TestCollectionDaoFindAllSortedByName(t *testing.T) { }) assert.Nil(t, err) assert.Equal(t, cs, []core.Collection{ - {Kind: "tag", Name: "science", NoteCount: 3}, - {Kind: "tag", Name: "history", NoteCount: 1}, - {Kind: "tag", Name: "fiction", NoteCount: 1}, - {Kind: "tag", Name: "fantasy", NoteCount: 1}, - {Kind: "tag", Name: "adventure", NoteCount: 2}, + {ID: 7, Kind: "tag", Name: "science", NoteCount: 3}, + {ID: 5, Kind: "tag", Name: "history", NoteCount: 1}, + {ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1}, + {ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1}, + {ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2}, }) }) } @@ -75,11 +75,11 @@ func TestCollectionDaoFindAllSortedByNoteCount(t *testing.T) { }) assert.Nil(t, err) assert.Equal(t, cs, []core.Collection{ - {Kind: "tag", Name: "science", NoteCount: 3}, - {Kind: "tag", Name: "adventure", NoteCount: 2}, - {Kind: "tag", Name: "fantasy", NoteCount: 1}, - {Kind: "tag", Name: "fiction", NoteCount: 1}, - {Kind: "tag", Name: "history", NoteCount: 1}, + {ID: 7, Kind: "tag", Name: "science", NoteCount: 3}, + {ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2}, + {ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1}, + {ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1}, + {ID: 5, Kind: "tag", Name: "history", NoteCount: 1}, }) }) } diff --git a/internal/cli/filtering.go b/internal/cli/filtering.go index 351c4b5..43c2011 100644 --- a/internal/cli/filtering.go +++ b/internal/cli/filtering.go @@ -15,32 +15,32 @@ import ( // Filtering holds filtering options to select notes. type Filtering struct { - Path []string `group:filter arg optional placeholder:PATH help:"Find notes matching the given path, including its descendants."` - - Interactive bool `group:filter short:i help:"Select notes interactively with fzf."` - Limit int `group:filter short:n placeholder:COUNT help:"Limit the number of notes found."` - Match string `group:filter short:m placeholder:QUERY help:"Terms to search for in the notes."` - ExactMatch bool `group:filter short:e help:"Search for exact occurrences of the --match argument (case insensitive)."` - Exclude []string `group:filter short:x placeholder:PATH help:"Ignore notes matching the given path, including its descendants."` - Tag []string `group:filter short:t help:"Find notes tagged with the given tags."` - Mention []string `group:filter placeholder:PATH help:"Find notes mentioning the title of the given ones."` - MentionedBy []string `group:filter placeholder:PATH help:"Find notes whose title is mentioned in the given ones."` - LinkTo []string `group:filter short:l placeholder:PATH help:"Find notes which are linking to the given ones."` - NoLinkTo []string `group:filter placeholder:PATH help:"Find notes which are not linking to the given notes."` - LinkedBy []string `group:filter short:L placeholder:PATH help:"Find notes which are linked by the given ones."` - NoLinkedBy []string `group:filter placeholder:PATH help:"Find notes which are not linked by the given ones."` - Orphan bool `group:filter help:"Find notes which are not linked by any other note."` - Related []string `group:filter placeholder:PATH help:"Find notes which might be related to the given ones."` - MaxDistance int `group:filter placeholder:COUNT help:"Maximum distance between two linked notes."` - Recursive bool `group:filter short:r help:"Follow links recursively."` - Created string `group:filter placeholder:DATE help:"Find notes created on the given date."` - CreatedBefore string `group:filter placeholder:DATE help:"Find notes created before the given date."` - CreatedAfter string `group:filter placeholder:DATE help:"Find notes created after the given date."` - Modified string `group:filter placeholder:DATE help:"Find notes modified on the given date."` - ModifiedBefore string `group:filter placeholder:DATE help:"Find notes modified before the given date."` - ModifiedAfter string `group:filter placeholder:DATE help:"Find notes modified after the given date."` - - Sort []string `group:sort short:s placeholder:TERM help:"Order the notes by the given criterion."` + Path []string `kong:"group='filter',arg,optional,placeholder='PATH',help='Find notes matching the given path, including its descendants.'" json:"hrefs"` + + Interactive bool `kong:"group='filter',short='i',help='Select notes interactively with fzf.'" json:"-"` + Limit int `kong:"group='filter',short='n',placeholder='COUNT',help='Limit the number of notes found.'" json:"limit"` + Match string `kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match"` + ExactMatch bool `kong:"group='filter',short='e',help='Search for exact occurrences of the --match argument (case insensitive).'" json:"exactMatch"` + Exclude []string `kong:"group='filter',short='x',placeholder='PATH',help='Ignore notes matching the given path, including its descendants.'" json:"excludeHrefs"` + Tag []string `kong:"group='filter',short='t',help='Find notes tagged with the given tags.'" json:"tags"` + Mention []string `kong:"group='filter',placeholder='PATH',help='Find notes mentioning the title of the given ones.'" json:"mention"` + MentionedBy []string `kong:"group='filter',placeholder='PATH',help='Find notes whose title is mentioned in the given ones.'" json:"mentionedBy"` + LinkTo []string `kong:"group='filter',short='l',placeholder='PATH',help='Find notes which are linking to the given ones.'" json:"linkTo"` + NoLinkTo []string `kong:"group='filter',placeholder='PATH',help='Find notes which are not linking to the given notes.'" json:"-"` + LinkedBy []string `kong:"group='filter',short='L',placeholder='PATH',help='Find notes which are linked by the given ones.'" json:"linkedBy"` + NoLinkedBy []string `kong:"group='filter',placeholder='PATH',help='Find notes which are not linked by the given ones.'" json:"-"` + Orphan bool `kong:"group='filter',help='Find notes which are not linked by any other note.'" json:"orphan"` + Related []string `kong:"group='filter',placeholder='PATH',help='Find notes which might be related to the given ones.'" json:"related"` + MaxDistance int `kong:"group='filter',placeholder='COUNT',help='Maximum distance between two linked notes.'" json:"maxDistance"` + Recursive bool `kong:"group='filter',short='r',help='Follow links recursively.'" json:"recursive"` + Created string `kong:"group='filter',placeholder='DATE',help:'Find notes created on the given date.'" json:"created"` + CreatedBefore string `kong:"group='filter',placeholder='DATE',help='Find notes created before the given date.'" json:"createdBefore"` + CreatedAfter string `kong:"group='filter',placeholder='DATE',help='Find notes created after the given date.'" json:"createdAfter"` + Modified string `kong:"group='filter',placeholder='DATE',help='Find notes modified on the given date.'" json:"modified"` + ModifiedBefore string `kong:"group='filter',placeholder='DATE',help='Find notes modified before the given date.'" json:"modifiedBefore"` + ModifiedAfter string `kong:"group='filter',placeholder='DATE',help='Find notes modified after the given date.'" json:"modifiedAfter"` + + Sort []string `kong:"group='sort',short='s',placeholder='TERM',help='Order the notes by the given criterion.'" json:"sort"` } // ExpandNamedFilters expands recursively any named filter found in the Path field. diff --git a/internal/core/collection.go b/internal/core/collection.go index ff0924c..cc8c29f 100644 --- a/internal/core/collection.go +++ b/internal/core/collection.go @@ -9,13 +9,13 @@ import ( // Collection represents a collection, such as a tag. type Collection struct { // Unique ID of this collection in the Notebook. - ID CollectionID + ID CollectionID `json:"id"` // Kind of this note collection, such as a tag. - Kind CollectionKind + Kind CollectionKind `json:"kind"` // Name of this collection. - Name string + Name string `json:"name"` // Number of notes associated with this collection. - NoteCount int + NoteCount int `json:"note_count"` } // CollectionID represents the unique ID of a collection relative to a given diff --git a/internal/util/strings/strings.go b/internal/util/strings/strings.go index f204b00..518ce96 100644 --- a/internal/util/strings/strings.go +++ b/internal/util/strings/strings.go @@ -125,3 +125,14 @@ func ExpandWhitespaceLiterals(s string) string { s = strings.ReplaceAll(s, `\t`, "\t") return s } + +// Contains returns whether the given slice of strings contains the given +// string. +func Contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +}