Merge branch 'main' into feature/graph

pull/105/head
Mickaël Menu 3 years ago
commit efb606024b
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -2,7 +2,9 @@
All notable changes to this project will be documented in this file.
## Unreleased
<!-- ## Unreleased -->
## 0.6.0
### Added
@ -13,6 +15,15 @@ All notable changes to this project will be documented in this file.
* `{{json title}}` prints with quotes `"An interesting note"`
* `{{json .}}` serializes the full template context as a JSON object.
* Use `--header` and `--footer` options with `zk list` to print arbitrary text at the start or end of the list.
* Support for LSP references to browse the backlinks of the link under the caret (contributed by [@pstuifzand](https://github.com/mickael-menu/zk/pull/58)).
* New [`note.ignore`](docs/config-note.md) configuration option to ignore files matching the given path globs when indexing notes.
```yaml
[note]
ignore = [
"log-*.md"
"drafts/*"
]
```
### Fixed

@ -45,7 +45,6 @@ Make sure you have a working [Go installation](https://golang.org/), then clone
```sh
$ git clone https://github.com/mickael-menu/zk.git
$ cd zk
$ chmod a+x go
```
#### On macOS

@ -14,6 +14,8 @@ The `[note]` section from the [configuration file](config.md) is used to set the
* `template` (string)
* Path to the [template](template.md) used to generate the note content.
* Either an absolute path, or relative to `.zk/templates/`.
* `ignore` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) ignored during note indexing.
* `id-charset` (string)
* Characters set used to [generate random IDs](note-id.md).
* You can use:

@ -104,6 +104,8 @@ func NewServer(opts ServerOpts) *Server {
ResolveProvider: boolPtr(true),
}
capabilities.ReferencesProvider = &protocol.ReferenceOptions{}
return protocol.InitializeResult{
Capabilities: capabilities,
ServerInfo: &protocol.InitializeResultServerInfo{
@ -389,6 +391,71 @@ func NewServer(opts ServerOpts) *Server {
return actions, nil
}
handler.TextDocumentReferences = func(context *glsp.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
link, err := doc.DocumentLinkAt(params.Position)
if link == nil || err != nil {
return nil, err
}
notebook, err := server.notebookOf(doc)
if err != nil {
return nil, err
}
target, err := server.noteForHref(link.Href, doc, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
p, err := notebook.RelPath(target.Path)
if err != nil {
return nil, err
}
opts := core.NoteFindOpts{
LinkTo: &core.LinkFilter{Paths: []string{p}},
}
notes, err := notebook.FindNotes(opts)
if err != nil {
return nil, err
}
var locations []protocol.Location
for _, note := range notes {
pos := strings.Index(note.RawContent, target.Path[0:len(target.Path)-3])
var line uint32 = 0
if pos < 0 {
line = 0
} else {
linePos := strings.Count(note.RawContent[0:pos], "\n")
line = uint32(linePos)
}
locations = append(locations, protocol.Location{
URI: pathToURI(filepath.Join(notebook.Path, note.Path)),
Range: protocol.Range{
Start: protocol.Position{
Line: line,
Character: 0,
},
End: protocol.Position{
Line: line,
Character: 0,
},
},
})
}
return locations, nil
}
return server
}

@ -15,7 +15,7 @@ import (
type List struct {
Format string `group:format short:f placeholder:TEMPLATE help:"Pretty print the list using a custom template or one of the predefined formats: oneline, short, medium, long, full, json, jsonl."`
Header string `group:format help:"Arbitrary text printed at the start of the list."`
Footer string `group:format help:"Arbitrary text printed at the end of the list."`
Footer string `group:format default:\n help:"Arbitrary text printed at the end of the list."`
Delimiter string "group:format short:d default:\n help:\"Print notes delimited by the given separator.\""
Delimiter0 bool "group:format short:0 name:delimiter0 help:\"Print notes delimited by ASCII NUL characters. This is useful when used in conjunction with `xargs -0`.\""
NoPager bool `group:format short:P help:"Do not pipe output into a pager."`
@ -35,7 +35,7 @@ func (cmd *List) Run(container *cli.Container) error {
if cmd.Header != "" {
return errors.New("--footer and --delimiter0 can't be used together")
}
if cmd.Footer != "" {
if cmd.Footer != "\n" {
return errors.New("--footer and --delimiter0 can't be used together")
}
@ -47,7 +47,7 @@ func (cmd *List) Run(container *cli.Container) error {
if cmd.Header != "" {
return errors.New("--header can't be used with JSON format")
}
if cmd.Footer != "" {
if cmd.Footer != "\n" {
return errors.New("--footer can't be used with JSON format")
}
if cmd.Delimiter != "\n" {
@ -129,7 +129,7 @@ func (cmd *List) Run(container *cli.Container) error {
}
if err == nil && !cmd.Quiet {
fmt.Fprintf(os.Stderr, "\n\nFound %d %s\n", count, strings.Pluralize("note", count))
fmt.Fprintf(os.Stderr, "\nFound %d %s\n", count, strings.Pluralize("note", count))
}
return err

@ -3,6 +3,7 @@ package core
import (
"fmt"
"path/filepath"
"strings"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
@ -35,6 +36,7 @@ func NewDefaultConfig() Config {
Length: 4,
Case: CaseLower,
},
Ignore: []string{},
},
Groups: map[string]GroupConfig{},
Format: FormatConfig{
@ -68,6 +70,16 @@ func (c Config) RootGroupConfig() GroupConfig {
}
}
// GroupConfigForPath returns the GroupConfig for the group matching the given
// path relative to the notebook. Fallback on the root GroupConfig.
func (c Config) GroupConfigForPath(path string) (GroupConfig, error) {
name, err := c.GroupNameForPath(path)
if err != nil {
return GroupConfig{}, err
}
return c.GroupConfigNamed(name)
}
// GroupConfigNamed returns the GroupConfig for the group with the given name.
// An empty name matches the root GroupConfig.
func (c Config) GroupConfigNamed(name string) (GroupConfig, error) {
@ -93,6 +105,9 @@ func (c Config) GroupNameForPath(path string) (string, error) {
} else if matches {
return name, nil
}
if strings.HasPrefix(path, groupPath+"/") {
return name, nil
}
}
}
@ -166,6 +181,8 @@ type NoteConfig struct {
DefaultTitle string
// Settings used when generating a random ID.
IDOptions IDOptions
// Path globs to ignore when indexing notes.
Ignore []string
}
// GroupConfig holds the user configuration for a given group of notes.
@ -175,6 +192,22 @@ type GroupConfig struct {
Extra map[string]string
}
// IgnoreGlobs returns all the Note.Ignore path globs for the group paths,
// relative to the root of the notebook.
func (c GroupConfig) IgnoreGlobs() []string {
if len(c.Paths) == 0 {
return c.Note.Ignore
}
globs := []string{}
for _, p := range c.Paths {
for _, g := range c.Note.Ignore {
globs = append(globs, filepath.Join(p, g))
}
}
return globs
}
// Clone creates a copy of the GroupConfig receiver.
func (c GroupConfig) Clone() GroupConfig {
clone := c
@ -248,6 +281,9 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro
if note.DefaultTitle != "" {
config.Note.DefaultTitle = note.DefaultTitle
}
for _, v := range note.Ignore {
config.Note.Ignore = append(config.Note.Ignore, v)
}
if tomlConf.Extra != nil {
for k, v := range tomlConf.Extra {
config.Extra[k] = v
@ -375,6 +411,9 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig {
if note.DefaultTitle != "" {
res.Note.DefaultTitle = note.DefaultTitle
}
for _, v := range note.Ignore {
res.Note.Ignore = append(res.Note.Ignore, v)
}
if tomlConf.Extra != nil {
for k, v := range tomlConf.Extra {
res.Extra[k] = v
@ -400,11 +439,12 @@ type tomlNoteConfig struct {
Filename string
Extension string
Template string
Lang string `toml:"language"`
DefaultTitle string `toml:"default-title"`
IDCharset string `toml:"id-charset"`
IDLength int `toml:"id-length"`
IDCase string `toml:"id-case"`
Lang string `toml:"language"`
DefaultTitle string `toml:"default-title"`
IDCharset string `toml:"id-charset"`
IDLength int `toml:"id-length"`
IDCase string `toml:"id-case"`
Ignore []string `toml:"ignore"`
}
type tomlGroupConfig struct {

@ -25,6 +25,7 @@ func TestParseDefaultConfig(t *testing.T) {
},
DefaultTitle: "Untitled",
Lang: "en",
Ignore: []string{},
},
Groups: make(map[string]GroupConfig),
Format: FormatConfig{
@ -73,6 +74,7 @@ func TestParseComplete(t *testing.T) {
id-charset = "alphanum"
id-length = 4
id-case = "lower"
ignore = ["ignored", ".git"]
[format.markdown]
hashtags = false
@ -112,6 +114,7 @@ func TestParseComplete(t *testing.T) {
id-charset = "letters"
id-length = 8
id-case = "mixed"
ignore = ["new-ignored"]
[group.log.extra]
log-ext = "value"
@ -140,6 +143,7 @@ func TestParseComplete(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Groups: map[string]GroupConfig{
"log": {
@ -155,6 +159,7 @@ func TestParseComplete(t *testing.T) {
},
Lang: "de",
DefaultTitle: "Ohne Titel",
Ignore: []string{"ignored", ".git", "new-ignored"},
},
Extra: map[string]string{
"hello": "world",
@ -175,6 +180,7 @@ func TestParseComplete(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "world",
@ -194,6 +200,7 @@ func TestParseComplete(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "world",
@ -249,6 +256,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
id-charset = "letters"
id-length = 42
id-case = "upper"
ignore = ["ignored", ".git"]
[extra]
hello = "world"
@ -281,6 +289,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Groups: map[string]GroupConfig{
"log": {
@ -296,6 +305,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "override",
@ -316,6 +326,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "world",
@ -451,6 +462,33 @@ func TestParseLSPDiagnosticsSeverity(t *testing.T) {
assert.Err(t, err, "foobar: unknown LSP diagnostic severity - may be none, hint, info, warning or error")
}
func TestGroupConfigIgnoreGlobs(t *testing.T) {
// empty globs
config := GroupConfig{
Paths: []string{"path"},
Note: NoteConfig{Ignore: []string{}},
}
assert.Equal(t, config.IgnoreGlobs(), []string{})
// empty paths
config = GroupConfig{
Paths: []string{},
Note: NoteConfig{
Ignore: []string{"ignored", ".git"},
},
}
assert.Equal(t, config.IgnoreGlobs(), []string{"ignored", ".git"})
// several paths
config = GroupConfig{
Paths: []string{"log", "drafts"},
Note: NoteConfig{
Ignore: []string{"ignored", "*.git"},
},
}
assert.Equal(t, config.IgnoreGlobs(), []string{"log/ignored", "log/*.git", "drafts/ignored", "drafts/*.git"})
}
func TestGroupConfigClone(t *testing.T) {
original := GroupConfig{
Paths: []string{"original"},
@ -465,6 +503,7 @@ func TestGroupConfigClone(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "world",
@ -484,6 +523,7 @@ func TestGroupConfigClone(t *testing.T) {
clone.Note.IDOptions.Case = CaseUpper
clone.Note.Lang = "de"
clone.Note.DefaultTitle = "Ohne Titel"
clone.Note.Ignore = []string{"other-ignored"}
clone.Extra["test"] = "modified"
// Check that we didn't modify the original
@ -500,6 +540,7 @@ func TestGroupConfigClone(t *testing.T) {
},
Lang: "fr",
DefaultTitle: "Sans titre",
Ignore: []string{"ignored", ".git"},
},
Extra: map[string]string{
"hello": "world",

@ -98,8 +98,31 @@ func (t *indexTask) execute(callback func(change paths.DiffChange)) (NoteIndexin
force := t.force || needsReindexing
// FIXME: Use Extension defined in each DirConfig.
source := paths.Walk(t.notebook.Path, t.notebook.Config.Note.Extension, t.logger)
shouldIgnorePath := func(path string) (bool, error) {
group, err := t.notebook.Config.GroupConfigForPath(path)
if err != nil {
return true, err
}
if filepath.Ext(path) != "."+group.Note.Extension {
return true, nil
}
for _, ignoreGlob := range group.IgnoreGlobs() {
matches, err := filepath.Match(ignoreGlob, path)
if err != nil {
return true, errors.Wrapf(err, "failed to match ignore glob %s to %s", ignoreGlob, path)
}
if matches {
return true, nil
}
}
return false, nil
}
source := paths.Walk(t.notebook.Path, t.logger, shouldIgnorePath)
target, err := t.index.IndexedPaths()
if err != nil {
return stats, wrap(err)

@ -193,6 +193,12 @@ const defaultConfig = `# zk configuration file
# If not an absolute path, it is relative to .zk/templates/
template = "default.md"
# Path globs ignored while indexing existing notes.
#ignore = [
# "drafts/*",
# "log.md"
#]
# Configure random ID generation.
# The charset used for random IDs. You can use:

@ -8,11 +8,9 @@ import (
"github.com/mickael-menu/zk/internal/util"
)
// Walk emits the metadata of each file stored in the directory with the given extension.
// Hidden files and directories are ignored.
func Walk(basePath string, extension string, logger util.Logger) <-chan Metadata {
extension = "." + extension
// Walk emits the metadata of each file stored in the directory if they pass
// the given shouldIgnorePath closure. Hidden files and directories are ignored.
func Walk(basePath string, logger util.Logger, shouldIgnorePath func(string) (bool, error)) <-chan Metadata {
c := make(chan Metadata, 50)
go func() {
defer close(c)
@ -31,15 +29,19 @@ func Walk(basePath string, extension string, logger util.Logger) <-chan Metadata
}
} else {
if isHidden || filepath.Ext(filename) != extension {
path, err := filepath.Rel(basePath, abs)
if err != nil {
logger.Println(err)
return nil
}
path, err := filepath.Rel(basePath, abs)
shouldIgnore, err := shouldIgnorePath(path)
if err != nil {
logger.Println(err)
return nil
}
if isHidden || shouldIgnore {
return nil
}
c <- Metadata{
Path: path,

@ -1,6 +1,7 @@
package paths
import (
"path/filepath"
"testing"
"github.com/mickael-menu/zk/internal/util"
@ -11,8 +12,12 @@ import (
func TestWalk(t *testing.T) {
var path = fixtures.Path("walk")
shouldIgnore := func(path string) (bool, error) {
return filepath.Ext(path) != ".md", nil
}
actual := make([]string, 0)
for m := range Walk(path, "md", &util.NullLogger) {
for m := range Walk(path, &util.NullLogger, shouldIgnore) {
assert.NotNil(t, m.Modified)
actual = append(actual, m.Path)
}

Loading…
Cancel
Save