Add LSP commands `zk.list` and `zk.tag.list` (#114)

pull/115/head
Mickaël Menu 3 years ago committed by GitHub
parent 0e88685140
commit 9c06068cce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.

@ -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. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| 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` | location<sup>1</sup> | 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
</details>
`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. <details><summary>A dictionary of additional options (click to expand)</summary>
| Key | Type | Required? | Description |
|------------------|--------------|-----------|-------------------------------------------------------------------------|
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `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`.
</details>
`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. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Required? | Description |
|--------|--------------|-----------|--------------------------------------------------|
| `sort` | string array | No | Order the tags by the given criteria<sup>1</sup> |
1. The available sort criteria are `name` and `note-count`. You can change the order by appending `-` or `+` to the criterion.
</details>
`zk.tag.list` returns the tags as a JSON array.

@ -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)
}

@ -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 = &note.Created
}
if selection.Modified {
res.Modified = &note.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"`
}

@ -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, &noteExists) {
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
}

@ -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)
}

@ -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, &noteExists) {
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)
}

@ -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,

@ -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},
})
})
}

@ -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.

@ -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

@ -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
}

Loading…
Cancel
Save