diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21cb280..584c517 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,12 +6,20 @@ All notable changes to this project will be documented in this file.
### Added
+* [Editor integration through LSP](https://github.com/mickael-menu/zk/issues/22):
+ * New code actions to create a note using the current selection as title.
+ * Custom commands to [run `new` and `index` from your editor](docs/editors-integration.md#custom-commands).
* Customize the format of `fzf`'s lines [with your own template](docs/tool-fzf.md).
```toml
[tool]
fzf-line = "{{style 'green' path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
```
+### Changed
+
+* Automatically index the notebook when saving a note with an LSP-enabled editor.
+ * This ensures that tags and notes auto-completion lists are up-to-date.
+
### Fixed
* Creating a new note from `fzf` in a directory containing spaces.
diff --git a/docs/editors-integration.md b/docs/editors-integration.md
index 7e54cdd..cce9271 100644
--- a/docs/editors-integration.md
+++ b/docs/editors-integration.md
@@ -13,16 +13,21 @@ There are several extensions available to integrate `zk` in your favorite editor
* Auto-complete [hashtags and colon-separated tags](tags.md).
* Preview the content of a note when hovering a link.
* Navigate in your notes by following internal links.
+* Create a new note using the current selection as title.
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
+### Editor LSP configurations
+
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/mickael-menu/zk/issues/22).
-### Vim and Neovim
+#### Vim and Neovim
-#### Vim and Neovim 0.4
+##### Vim and Neovim 0.4
With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and add the following in the settings file:
+coc-settings.json
+
```jsonc
{
// Important, otherwise link completion containing spaces and other special characters won't work.
@@ -38,11 +43,39 @@ With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and ad
}
}
```
+
-#### Neovim 0.5 built-in LSP client
+Here are some additional useful key bindings and custom commands:
+
+~/.config/nvim/init.vim
+
+```viml
+" User command to index the current notebook.
+"
+" zk.index expects a notebook path as first argument, so we provide the current
+" buffer path with expand("%:p").
+command! -nargs=0 ZkIndex :call CocAction("runCommand", "zk.index", expand("%:p"))
+nnoremap zi :ZkIndex
+
+" User command to create and open a new note, to be called like this:
+" :ZkNew {"title": "An interesting subject", "dir": "inbox", ...}
+"
+" Note the concatenation with the "edit" command to open the note right away.
+command! -nargs=? ZkNew :exec "edit ".CocAction("runCommand", "zk.new", expand("%:p"), ).path
+
+" Create a new note after prompting for its title.
+nnoremap zn :ZkNew {"title": input("Title: ")}
+" Create a new note in the directory journal/daily.
+nnoremap zj :ZkNew {"dir": "journal/daily"}
+```
+
+
+##### Neovim 0.5 built-in LSP client
Using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig):
+~/.config/nvim/init.lua
+
```lua
local lspconfig = require('lspconfig')
local configs = require('lspconfig/configs')
@@ -62,11 +95,14 @@ lspconfig.zk.setup({ on_attach = function(client, buffer)
-- Add keybindings here, see https://github.com/neovim/nvim-lspconfig#keybindings-and-completion
end })
```
+
-### Sublime Text
+#### Sublime Text
Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run the **Preferences: LSP Settings** command. Add the following to the settings file:
+LSP.sublime-settings
+
```jsonc
{
"clients": {
@@ -80,7 +116,60 @@ Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run t
}
}
```
+
-### Visual Studio Code
+#### Visual Studio Code
Install the [`zk-vscode`](https://marketplace.visualstudio.com/items?itemName=mickael-menu.zk-vscode) extension from the Marketplace.
+
+### Custom commands
+
+Using `zk`'s LSP custom commands, you can call `zk` commands right from your editor. Please refer to your editor's documentation on how to bind keyboard shortcuts to custom LSP commands.
+
+#### `zk.index`
+
+This LSP command calls `zk index` to refresh your notebook's index. It can be useful to make sure that the auto-completion is up-to-date. `zk.index` takes two arguments:
+
+1. A path to a file or directory in the notebook to index.
+2. (Optional) A dictionary of additional options (click to expand)
+
+ | Key | Type | Description |
+ |---------|---------|-----------------------------------|
+ | `force` | boolean | Reindexes all the notes when true |
+
+
+`zk.index` returns a dictionary of indexing statistics.
+
+#### `zk.new`
+
+This LSP command calls `zk new` to create a new note. It can be useful to quickly create a new note with a key binding. `zk.new` 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 | 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 |
+
+ The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
+
+ ```json
+ {
+ "uri":"file:///Users/mickael/notes/9se3.md",
+ "range": {
+ "end":{"line": 5, "character":149},
+ "start":{"line": 5, "character":137}
+ }
+ }
+ ```
+
+
+`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file.
diff --git a/internal/adapter/lsp/document.go b/internal/adapter/lsp/document.go
index b829666..116130c 100644
--- a/internal/adapter/lsp/document.go
+++ b/internal/adapter/lsp/document.go
@@ -5,15 +5,71 @@ import (
"regexp"
"strings"
+ "github.com/mickael-menu/zk/internal/core"
+ "github.com/mickael-menu/zk/internal/util"
+ "github.com/mickael-menu/zk/internal/util/errors"
protocol "github.com/tliron/glsp/protocol_3_16"
- "github.com/tliron/kutil/logging"
)
+// documentStore holds opened documents.
+type documentStore struct {
+ documents map[string]*document
+ fs core.FileStorage
+ logger util.Logger
+}
+
+func newDocumentStore(fs core.FileStorage, logger util.Logger) *documentStore {
+ return &documentStore{
+ documents: map[string]*document{},
+ fs: fs,
+ logger: logger,
+ }
+}
+
+func (s *documentStore) DidOpen(params protocol.DidOpenTextDocumentParams) error {
+ langID := params.TextDocument.LanguageID
+ if langID != "markdown" && langID != "vimwiki" {
+ return nil
+ }
+
+ path, err := s.normalizePath(params.TextDocument.URI)
+ if err != nil {
+ return err
+ }
+ s.documents[path] = &document{
+ Path: path,
+ Content: params.TextDocument.Text,
+ }
+
+ return nil
+}
+
+func (s *documentStore) Close(uri protocol.DocumentUri) {
+ delete(s.documents, uri)
+}
+
+func (s *documentStore) Get(pathOrURI string) (*document, bool) {
+ path, err := s.normalizePath(pathOrURI)
+ if err != nil {
+ s.logger.Err(err)
+ return nil, false
+ }
+ d, ok := s.documents[path]
+ return d, ok
+}
+
+func (s *documentStore) normalizePath(pathOrUri string) (string, error) {
+ path, err := uriToPath(pathOrUri)
+ if err != nil {
+ return "", errors.Wrapf(err, "unable to parse URI: %s", pathOrUri)
+ }
+ return s.fs.Canonical(path), nil
+}
+
// document represents an opened file.
type document struct {
Path string
Content string
- Log logging.Logger
lines []string
}
@@ -53,6 +109,12 @@ func (d *document) WordAt(pos protocol.Position) string {
return ""
}
+// ContentAtRange returns the document text at given range.
+func (d *document) ContentAtRange(rng protocol.Range) string {
+ startIndex, endIndex := rng.IndexesIn(d.Content)
+ return d.Content[startIndex:endIndex]
+}
+
// GetLine returns the line at the given index.
func (d *document) GetLine(index int) (string, bool) {
lines := d.GetLines()
diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go
index a545d88..5652435 100644
--- a/internal/adapter/lsp/server.go
+++ b/internal/adapter/lsp/server.go
@@ -1,6 +1,7 @@
package lsp
import (
+ "encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
@@ -8,6 +9,7 @@ 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"
@@ -22,7 +24,7 @@ import (
type Server struct {
server *glspserv.Server
notebooks *core.NotebookStore
- documents map[protocol.DocumentUri]*document
+ documents *documentStore
fs core.FileStorage
logger util.Logger
}
@@ -45,20 +47,21 @@ func NewServer(opts ServerOpts) *Server {
logging.Configure(10, opts.LogFile.Value)
}
- workspace := newWorkspace()
handler := protocol.Handler{}
- server := &Server{
- server: glspserv.NewServer(&handler, opts.Name, debug),
- notebooks: opts.Notebooks,
- documents: map[string]*document{},
- fs: fs,
- }
+ glspServer := glspserv.NewServer(&handler, opts.Name, debug)
// Redirect zk's logger to GLSP's to avoid breaking the JSON-RPC protocol
// with unwanted output.
if opts.Logger != nil {
- opts.Logger.Logger = newGlspLogger(server.server.Log)
- server.logger = opts.Logger
+ opts.Logger.Logger = newGlspLogger(glspServer.Log)
+ }
+
+ server := &Server{
+ server: glspServer,
+ notebooks: opts.Notebooks,
+ documents: newDocumentStore(fs, opts.Logger),
+ fs: fs,
+ logger: opts.Logger,
}
var clientCapabilities protocol.ClientCapabilities
@@ -66,16 +69,6 @@ func NewServer(opts ServerOpts) *Server {
handler.Initialize = func(context *glsp.Context, params *protocol.InitializeParams) (interface{}, error) {
clientCapabilities = params.Capabilities
- if len(params.WorkspaceFolders) > 0 {
- for _, f := range params.WorkspaceFolders {
- workspace.addFolder(f.URI)
- }
- } else if params.RootURI != nil {
- workspace.addFolder(*params.RootURI)
- } else if params.RootPath != nil {
- workspace.addFolder(*params.RootPath)
- }
-
// To see the logs with coc.nvim, run :CocCommand workspace.showOutput
// https://github.com/neoclide/coc.nvim/wiki/Debug-language-server#using-output-channel
if params.Trace != nil {
@@ -84,6 +77,8 @@ func NewServer(opts ServerOpts) *Server {
capabilities := handler.CreateServerCapabilities()
capabilities.HoverProvider = true
+ capabilities.DefinitionProvider = true
+ capabilities.CodeActionProvider = true
change := protocol.TextDocumentSyncKindIncremental
capabilities.TextDocumentSync = protocol.TextDocumentSyncOptions{
@@ -97,13 +92,17 @@ func NewServer(opts ServerOpts) *Server {
triggerChars := []string{"[", "#", ":"}
+ capabilities.ExecuteCommandProvider = &protocol.ExecuteCommandOptions{
+ Commands: []string{
+ cmdIndex,
+ cmdNew,
+ },
+ }
capabilities.CompletionProvider = &protocol.CompletionOptions{
TriggerCharacters: triggerChars,
ResolveProvider: boolPtr(true),
}
- capabilities.DefinitionProvider = boolPtr(true)
-
return protocol.InitializeResult{
Capabilities: capabilities,
ServerInfo: &protocol.InitializeResultServerInfo{
@@ -127,40 +126,12 @@ func NewServer(opts ServerOpts) *Server {
return nil
}
- handler.WorkspaceDidChangeWorkspaceFolders = func(context *glsp.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
- for _, f := range params.Event.Added {
- workspace.addFolder(f.URI)
- }
- for _, f := range params.Event.Removed {
- workspace.removeFolder(f.URI)
- }
- return nil
- }
-
handler.TextDocumentDidOpen = func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error {
- langID := params.TextDocument.LanguageID
- if langID != "markdown" && langID != "vimwiki" {
- return nil
- }
-
- path, err := uriToPath(params.TextDocument.URI)
- if err != nil {
- server.logger.Printf("unable to parse URI: %v", err)
- return nil
- }
- path = fs.Canonical(path)
-
- server.documents[params.TextDocument.URI] = &document{
- Path: path,
- Content: params.TextDocument.Text,
- Log: server.server.Log,
- }
-
- return nil
+ return server.documents.DidOpen(*params)
}
handler.TextDocumentDidChange = func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error {
- doc, ok := server.documents[params.TextDocument.URI]
+ doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil
}
@@ -170,11 +141,24 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentDidClose = func(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
- delete(server.documents, params.TextDocument.URI)
+ server.documents.Close(params.TextDocument.URI)
return nil
}
handler.TextDocumentDidSave = func(context *glsp.Context, params *protocol.DidSaveTextDocumentParams) error {
+ doc, ok := server.documents.Get(params.TextDocument.URI)
+ if !ok {
+ return nil
+ }
+
+ notebook, err := server.notebookOf(doc)
+ if err != nil {
+ server.logger.Err(err)
+ return nil
+ }
+
+ _, err = notebook.Index(false)
+ server.logger.Err(err)
return nil
}
@@ -184,7 +168,7 @@ func NewServer(opts ServerOpts) *Server {
return nil, nil
}
- doc, ok := server.documents[params.TextDocument.URI]
+ doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@@ -228,7 +212,7 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentHover = func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
- doc, ok := server.documents[params.TextDocument.URI]
+ doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@@ -269,7 +253,7 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentDocumentLink = func(context *glsp.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
- doc, ok := server.documents[params.TextDocument.URI]
+ doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@@ -301,7 +285,7 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentDefinition = func(context *glsp.Context, params *protocol.DefinitionParams) (interface{}, error) {
- doc, ok := server.documents[params.TextDocument.URI]
+ doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@@ -335,9 +319,201 @@ func NewServer(opts ServerOpts) *Server {
}
}
+ handler.WorkspaceExecuteCommand = func(context *glsp.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
+ switch params.Command {
+ case cmdIndex:
+ return server.executeCommandIndex(params.Arguments)
+ case cmdNew:
+ return server.executeCommandNew(context, params.Arguments)
+ default:
+ return nil, fmt.Errorf("unknown zk LSP command: %s", params.Command)
+ }
+ }
+
+ handler.TextDocumentCodeAction = func(context *glsp.Context, params *protocol.CodeActionParams) (interface{}, error) {
+ if isRangeEmpty(params.Range) {
+ return nil, nil
+ }
+
+ doc, ok := server.documents.Get(params.TextDocument.URI)
+ if !ok {
+ return nil, nil
+ }
+ wd := filepath.Dir(doc.Path)
+
+ actions := []protocol.CodeAction{}
+
+ addAction := func(dir string, actionTitle string) error {
+ opts := cmdNewOpts{
+ Title: doc.ContentAtRange(params.Range),
+ Dir: dir,
+ InsertLinkAtLocation: &protocol.Location{
+ URI: params.TextDocument.URI,
+ Range: params.Range,
+ },
+ }
+
+ var jsonOpts map[string]interface{}
+ err := unmarshalJSON(opts, &jsonOpts)
+ if err != nil {
+ return err
+ }
+
+ actions = append(actions, protocol.CodeAction{
+ Title: actionTitle,
+ Kind: stringPtr(protocol.CodeActionKindRefactor),
+ Command: &protocol.Command{
+ Command: cmdNew,
+ Arguments: []interface{}{wd, jsonOpts},
+ },
+ })
+
+ return nil
+ }
+
+ addAction(wd, "New note in current directory")
+ addAction("", "New note in top directory")
+
+ return actions, nil
+ }
+
return server
}
+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])
+ }
+
+ force := false
+ 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 {
+ force = toBool(forceOption)
+ }
+ }
+
+ notebook, err := s.notebooks.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ return notebook.Index(force)
+}
+
+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)
+ }
+
+ path, 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
+ }
+ path = noteExists.Path
+ }
+
+ // Index the notebook to be able to navigate to the new note.
+ notebook.Index(false)
+
+ 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
+ }
+
+ relPath, err := filepath.Rel(filepath.Dir(doc.Path), path)
+ if err != nil {
+ return nil, err
+ }
+
+ link, err := linkFormatter(relPath, opts.Title)
+ 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)
+ }
+
+ if opts.Edit {
+ go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
+ URI: "file://" + path,
+ TakeFocus: boolPtr(true),
+ }, nil)
+ }
+
+ return map[string]interface{}{"path": path}, nil
+}
+
func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
return s.notebooks.Open(doc.Path)
}
@@ -513,6 +689,10 @@ func rangeFromPosition(pos protocol.Position, startOffset, endOffset int) protoc
}
}
+func isRangeEmpty(pos protocol.Range) bool {
+ return pos.Start == pos.End
+}
+
func boolPtr(v bool) *bool {
b := v
return &b
@@ -530,3 +710,16 @@ func stringPtr(v string) *string {
s := v
return &s
}
+
+func unmarshalJSON(obj interface{}, v interface{}) error {
+ js, err := json.Marshal(obj)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(js, v)
+}
+
+func toBool(obj interface{}) bool {
+ s := strings.ToLower(fmt.Sprint(obj))
+ return s == "true" || s == "1"
+}
diff --git a/internal/adapter/lsp/util.go b/internal/adapter/lsp/util.go
index 2a064ff..493c0a2 100644
--- a/internal/adapter/lsp/util.go
+++ b/internal/adapter/lsp/util.go
@@ -1,19 +1,20 @@
package lsp
import (
+ "fmt"
"net/url"
+
"github.com/mickael-menu/zk/internal/util/errors"
)
func pathToURI(path string) string {
u := &url.URL{
- Scheme: "file",
- Path: path,
+ Scheme: "file",
+ Path: path,
}
return u.String()
}
-
func uriToPath(uri string) (string, error) {
parsed, err := url.Parse(uri)
if err != nil {
@@ -25,3 +26,18 @@ func uriToPath(uri string) (string, error) {
return parsed.Path, nil
}
+// jsonBoolean can be unmarshalled from integers or strings.
+// Neovim cannot send a boolean easily, so it's useful to support integers too.
+type jsonBoolean bool
+
+func (b *jsonBoolean) UnmarshalJSON(data []byte) error {
+ s := string(data)
+ if s == "1" || s == "true" {
+ *b = true
+ } else if s == "0" || s == "false" {
+ *b = false
+ } else {
+ return fmt.Errorf("%s: failed to unmarshal as boolean", s)
+ }
+ return nil
+}
diff --git a/internal/adapter/lsp/workspace.go b/internal/adapter/lsp/workspace.go
deleted file mode 100644
index e713613..0000000
--- a/internal/adapter/lsp/workspace.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package lsp
-
-import "strings"
-
-type workspace struct {
- folders []string
-}
-
-func newWorkspace() *workspace {
- return &workspace{
- folders: []string{},
- }
-}
-
-func (w *workspace) addFolder(folder string) {
- folder = strings.TrimPrefix(folder, "file://")
- w.folders = append(w.folders, folder)
-}
-
-func (w *workspace) removeFolder(folder string) {
- folder = strings.TrimPrefix(folder, "file://")
- for i, f := range w.folders {
- if f == folder {
- w.folders = append(w.folders[:i], w.folders[i+1:]...)
- break
- }
- }
-}
diff --git a/internal/cli/filtering.go b/internal/cli/filtering.go
index c6fc7f0..0596b24 100644
--- a/internal/cli/filtering.go
+++ b/internal/cli/filtering.go
@@ -2,16 +2,15 @@ package cli
import (
"fmt"
- "strconv"
"time"
"github.com/alecthomas/kong"
"github.com/kballard/go-shellquote"
"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/mickael-menu/zk/internal/util/strings"
- "github.com/tj/go-naturaldate"
)
// Filtering holds filtering options to select notes.
@@ -205,14 +204,14 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
opts.CreatedEnd = &end
} else {
if f.CreatedBefore != "" {
- date, err := parseDate(f.CreatedBefore)
+ date, err := dateutil.TimeFromNatural(f.CreatedBefore)
if err != nil {
return opts, err
}
opts.CreatedEnd = &date
}
if f.CreatedAfter != "" {
- date, err := parseDate(f.CreatedAfter)
+ date, err := dateutil.TimeFromNatural(f.CreatedAfter)
if err != nil {
return opts, err
}
@@ -229,14 +228,14 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
opts.ModifiedEnd = &end
} else {
if f.ModifiedBefore != "" {
- date, err := parseDate(f.ModifiedBefore)
+ date, err := dateutil.TimeFromNatural(f.ModifiedBefore)
if err != nil {
return opts, err
}
opts.ModifiedEnd = &date
}
if f.ModifiedAfter != "" {
- date, err := parseDate(f.ModifiedAfter)
+ date, err := dateutil.TimeFromNatural(f.ModifiedAfter)
if err != nil {
return opts, err
}
@@ -266,15 +265,8 @@ func relPaths(notebook *core.Notebook, paths []string) ([]string, bool) {
return relPaths, len(relPaths) > 0
}
-func parseDate(date string) (time.Time, error) {
- if i, err := strconv.ParseInt(date, 10, 0); err == nil && i >= 1000 && i < 5000 {
- return time.Date(int(i), time.January, 0, 0, 0, 0, 0, time.UTC), nil
- }
- return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))
-}
-
func parseDayRange(date string) (start time.Time, end time.Time, err error) {
- day, err := parseDate(date)
+ day, err := dateutil.TimeFromNatural(date)
if err != nil {
return
}
diff --git a/internal/core/note_index.go b/internal/core/note_index.go
index d7f70fd..5539ead 100644
--- a/internal/core/note_index.go
+++ b/internal/core/note_index.go
@@ -49,15 +49,15 @@ type NoteIndex interface {
// NoteIndexingStats holds statistics about a notebook indexing process.
type NoteIndexingStats struct {
// Number of notes in the source.
- SourceCount int
+ SourceCount int `json:"sourceCount"`
// Number of newly indexed notes.
- AddedCount int
+ AddedCount int `json:"addedCount"`
// Number of notes modified since last indexing.
- ModifiedCount int
+ ModifiedCount int `json:"modifiedCount"`
// Number of notes removed since last indexing.
- RemovedCount int
+ RemovedCount int `json:"removedCount"`
// Duration of the indexing process.
- Duration time.Duration
+ Duration time.Duration `json:"duration"`
}
// String implements Stringer
diff --git a/internal/util/date/date.go b/internal/util/date/date.go
index c1afe0a..b3d774a 100644
--- a/internal/util/date/date.go
+++ b/internal/util/date/date.go
@@ -1,6 +1,11 @@
package date
-import "time"
+import (
+ "strconv"
+ "time"
+
+ "github.com/tj/go-naturaldate"
+)
// Provider returns a date instance.
type Provider interface {
@@ -30,3 +35,14 @@ func NewFrozen(date time.Time) Frozen {
func (n *Frozen) Date() time.Time {
return n.date
}
+
+// TimeFromNatural parses a human date into a time.Time.
+func TimeFromNatural(date string) (time.Time, error) {
+ if date == "" {
+ return time.Now(), nil
+ }
+ if i, err := strconv.ParseInt(date, 10, 0); err == nil && i >= 1000 && i < 5000 {
+ return time.Date(int(i), time.January, 0, 0, 0, 0, 0, time.UTC), nil
+ }
+ return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))
+}
diff --git a/internal/util/errors/errors.go b/internal/util/errors/errors.go
index 9d52cb1..e8d2ffe 100644
--- a/internal/util/errors/errors.go
+++ b/internal/util/errors/errors.go
@@ -29,3 +29,7 @@ func Wrap(err error, msg string) error {
func New(text string) error {
return errors.New(text)
}
+
+func As(err error, target interface{}) bool {
+ return errors.As(err, target)
+}