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