Add LSP custom commands and code actions to create new notes (#40)

This commit is contained in:
Mickaël Menu 2021-05-11 21:53:19 +02:00 committed by GitHub
parent 3664734bda
commit b17b42a06f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 467 additions and 115 deletions

View File

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

View File

@ -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:
<details><summary><tt>coc-settings.json</tt></summary>
```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
}
}
```
</details>
#### Neovim 0.5 built-in LSP client
Here are some additional useful key bindings and custom commands:
<details><summary><tt>~/.config/nvim/init.vim</tt></summary>
```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 <leader>zi :ZkIndex<CR>
" 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"), <args>).path
" Create a new note after prompting for its title.
nnoremap <leader>zn :ZkNew {"title": input("Title: ")}<CR>
" Create a new note in the directory journal/daily.
nnoremap <leader>zj :ZkNew {"dir": "journal/daily"}<CR>
```
</details>
##### Neovim 0.5 built-in LSP client
Using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig):
<details><summary><tt>~/.config/nvim/init.lua</tt></summary>
```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 })
```
</details>
### 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:
<details><summary><tt>LSP.sublime-settings</tt></summary>
```jsonc
{
"clients": {
@ -80,7 +116,60 @@ Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run t
}
}
```
</details>
### 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. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Description |
|---------|---------|-----------------------------------|
| `force` | boolean | Reindexes all the notes when true |
</details>
`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. <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 |
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}
}
}
```
</details>
`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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