mirror of
https://github.com/mickael-menu/zk
synced 2024-11-15 12:12:56 +00:00
0eaf26483f
Finalise transfer from old repo (github.com/mickael-menu/zk) to new (github.com/zk-org/zk) Co-authored-by: tjex <tjex@tjex.net>
614 lines
16 KiB
Go
614 lines
16 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/zk-org/zk/internal/util/errors"
|
|
"github.com/zk-org/zk/internal/util/opt"
|
|
toml "github.com/pelletier/go-toml"
|
|
)
|
|
|
|
// Config holds the user configuration.
|
|
type Config struct {
|
|
Notebook NotebookConfig
|
|
Note NoteConfig
|
|
Groups map[string]GroupConfig
|
|
Format FormatConfig
|
|
Tool ToolConfig
|
|
LSP LSPConfig
|
|
Filters map[string]string
|
|
Aliases map[string]string
|
|
Extra map[string]string
|
|
}
|
|
|
|
// NewDefaultConfig creates a new Config with the default settings.
|
|
func NewDefaultConfig() Config {
|
|
return Config{
|
|
Notebook: NotebookConfig{
|
|
Dir: opt.NullString,
|
|
},
|
|
Note: NoteConfig{
|
|
FilenameTemplate: "{{id}}",
|
|
Extension: "md",
|
|
BodyTemplatePath: opt.NullString,
|
|
Lang: "en",
|
|
DefaultTitle: "Untitled",
|
|
IDOptions: IDOptions{
|
|
Charset: CharsetAlphanum,
|
|
Length: 4,
|
|
Case: CaseLower,
|
|
},
|
|
Exclude: []string{},
|
|
},
|
|
Groups: map[string]GroupConfig{},
|
|
Format: FormatConfig{
|
|
Markdown: MarkdownConfig{
|
|
Hashtags: true,
|
|
ColonTags: false,
|
|
MultiwordTags: false,
|
|
LinkFormat: "markdown",
|
|
LinkEncodePath: true,
|
|
LinkDropExtension: true,
|
|
},
|
|
},
|
|
LSP: LSPConfig{
|
|
Completion: LSPCompletionConfig{
|
|
Note: LSPCompletionTemplates{
|
|
Label: opt.NullString,
|
|
FilterText: opt.NullString,
|
|
Detail: opt.NullString,
|
|
},
|
|
},
|
|
Diagnostics: LSPDiagnosticConfig{
|
|
WikiTitle: LSPDiagnosticNone,
|
|
DeadLink: LSPDiagnosticError,
|
|
},
|
|
},
|
|
Filters: map[string]string{},
|
|
Aliases: map[string]string{},
|
|
Extra: map[string]string{},
|
|
}
|
|
}
|
|
|
|
// RootGroupConfig returns the default GroupConfig for the root directory and its descendants.
|
|
func (c Config) RootGroupConfig() GroupConfig {
|
|
return GroupConfig{
|
|
Paths: []string{},
|
|
Note: c.Note,
|
|
Extra: c.Extra,
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if name == "" {
|
|
return c.RootGroupConfig(), nil
|
|
} else {
|
|
group, ok := c.Groups[name]
|
|
if !ok {
|
|
return GroupConfig{}, fmt.Errorf("no group named `%s` found in the config", name)
|
|
}
|
|
return group, nil
|
|
}
|
|
}
|
|
|
|
// GroupNameForPath returns the name of the GroupConfig matching the given
|
|
// path, relative to the notebook.
|
|
func (c Config) GroupNameForPath(path string) (string, error) {
|
|
for name, config := range c.Groups {
|
|
for _, groupPath := range config.Paths {
|
|
matches, err := filepath.Match(groupPath, path)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "failed to match group %s to %s", name, path)
|
|
} else if matches {
|
|
return name, nil
|
|
}
|
|
if strings.HasPrefix(path, groupPath+"/") {
|
|
return name, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
// FormatConfig holds the configuration for document formats, such as Markdown.
|
|
type FormatConfig struct {
|
|
Markdown MarkdownConfig
|
|
}
|
|
|
|
// MarkdownConfig holds the configuration for Markdown documents.
|
|
type MarkdownConfig struct {
|
|
// Hashtags indicates whether #hashtags are supported.
|
|
Hashtags bool
|
|
// ColonTags indicates whether :colon:tags: are supported.
|
|
ColonTags bool
|
|
// MultiwordTags indicates whether #multi-word tags# are supported.
|
|
MultiwordTags bool
|
|
|
|
// Format used to generate links between notes.
|
|
// Either "wiki", "markdown" or a custom template. Default is "markdown".
|
|
LinkFormat string
|
|
// Indicates whether a link's path will be percent-encoded.
|
|
// Defaults to true for "markdown" format only, false otherwise.
|
|
LinkEncodePath bool
|
|
// Indicates whether a link's path file extension will be removed.
|
|
LinkDropExtension bool
|
|
}
|
|
|
|
// ToolConfig holds the external tooling configuration.
|
|
type ToolConfig struct {
|
|
Editor opt.String
|
|
Shell opt.String
|
|
Pager opt.String
|
|
FzfPreview opt.String
|
|
FzfLine opt.String
|
|
FzfOptions opt.String
|
|
FzfBindNew opt.String
|
|
}
|
|
|
|
// LSPConfig holds the Language Server Protocol configuration.
|
|
type LSPConfig struct {
|
|
Completion LSPCompletionConfig
|
|
Diagnostics LSPDiagnosticConfig
|
|
}
|
|
|
|
// LSPCompletionConfig holds the LSP auto-completion configuration.
|
|
type LSPCompletionConfig struct {
|
|
Note LSPCompletionTemplates
|
|
UseAdditionalTextEdits opt.Bool
|
|
}
|
|
|
|
// LSPCompletionConfig holds the LSP completion templates for a particular
|
|
// completion item type (e.g. note or tag).
|
|
type LSPCompletionTemplates struct {
|
|
Label opt.String
|
|
FilterText opt.String
|
|
Detail opt.String
|
|
}
|
|
|
|
// LSPDiagnosticConfig holds the LSP diagnostics configuration.
|
|
type LSPDiagnosticConfig struct {
|
|
WikiTitle LSPDiagnosticSeverity
|
|
DeadLink LSPDiagnosticSeverity
|
|
}
|
|
|
|
type LSPDiagnosticSeverity int
|
|
|
|
const (
|
|
LSPDiagnosticNone LSPDiagnosticSeverity = 0
|
|
LSPDiagnosticError LSPDiagnosticSeverity = 1
|
|
LSPDiagnosticWarning LSPDiagnosticSeverity = 2
|
|
LSPDiagnosticInfo LSPDiagnosticSeverity = 3
|
|
LSPDiagnosticHint LSPDiagnosticSeverity = 4
|
|
)
|
|
|
|
// NotebookConfig holds configuration about the default notebook
|
|
type NotebookConfig struct {
|
|
Dir opt.String
|
|
}
|
|
|
|
// NoteConfig holds the user configuration used when generating new notes.
|
|
type NoteConfig struct {
|
|
// Handlebars template used when generating a new filename.
|
|
FilenameTemplate string
|
|
// Extension appended to the filename.
|
|
Extension string
|
|
// Path to the handlebars template used when generating the note content.
|
|
BodyTemplatePath opt.String
|
|
// Language of the note content.
|
|
Lang string
|
|
// Default title to use when none is provided.
|
|
DefaultTitle string
|
|
// Settings used when generating a random ID.
|
|
IDOptions IDOptions
|
|
// Path globs to ignore when indexing notes.
|
|
Exclude []string
|
|
}
|
|
|
|
// GroupConfig holds the user configuration for a given group of notes.
|
|
type GroupConfig struct {
|
|
Paths []string
|
|
Note NoteConfig
|
|
Extra map[string]string
|
|
}
|
|
|
|
// ExcludeGlobs returns all the Note.Exclude path globs for the group paths,
|
|
// relative to the root of the notebook.
|
|
func (c GroupConfig) ExcludeGlobs() []string {
|
|
if len(c.Paths) == 0 {
|
|
return c.Note.Exclude
|
|
}
|
|
|
|
globs := []string{}
|
|
for _, p := range c.Paths {
|
|
for _, g := range c.Note.Exclude {
|
|
globs = append(globs, filepath.Join(p, g))
|
|
}
|
|
}
|
|
return globs
|
|
}
|
|
|
|
// Clone creates a copy of the GroupConfig receiver.
|
|
func (c GroupConfig) Clone() GroupConfig {
|
|
clone := c
|
|
|
|
clone.Paths = make([]string, len(c.Paths))
|
|
copy(clone.Paths, c.Paths)
|
|
|
|
clone.Extra = make(map[string]string)
|
|
for k, v := range c.Extra {
|
|
clone.Extra[k] = v
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// OpenConfig creates a new Config instance from its TOML representation stored
|
|
// in the given file.
|
|
func OpenConfig(path string, parentConfig Config, fs FileStorage, isGlobal bool) (Config, error) {
|
|
// The local config is optional.
|
|
exists, err := fs.FileExists(path)
|
|
if err == nil && !exists {
|
|
return parentConfig, nil
|
|
}
|
|
|
|
content, err := fs.Read(path)
|
|
if err != nil {
|
|
return parentConfig, errors.Wrapf(err, "failed to open config file at %s", path)
|
|
}
|
|
|
|
return ParseConfig(content, path, parentConfig, isGlobal)
|
|
}
|
|
|
|
// ParseConfig creates a new Config instance from its TOML representation.
|
|
// path is the config absolute path, from which will be derived the base path
|
|
// for templates.
|
|
//
|
|
// The parentConfig will be used to inherit default config settings.
|
|
func ParseConfig(content []byte, path string, parentConfig Config, isGlobal bool) (Config, error) {
|
|
wrap := errors.Wrapperf("failed to read config")
|
|
|
|
config := parentConfig
|
|
|
|
var tomlConf tomlConfig
|
|
err := toml.Unmarshal(content, &tomlConf)
|
|
if err != nil {
|
|
return config, wrap(err)
|
|
}
|
|
|
|
// Notebook
|
|
notebook := tomlConf.Notebook
|
|
if notebook.Dir != "" {
|
|
if isGlobal {
|
|
config.Notebook.Dir = opt.NewNotEmptyString(notebook.Dir)
|
|
} else {
|
|
return config, wrap(errors.New("notebook.dir should not be set on local configuration"))
|
|
}
|
|
}
|
|
|
|
// Note
|
|
note := tomlConf.Note
|
|
if note.Filename != "" {
|
|
config.Note.FilenameTemplate = note.Filename
|
|
}
|
|
if note.Extension != "" {
|
|
config.Note.Extension = note.Extension
|
|
}
|
|
if note.Template != "" {
|
|
config.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
|
|
}
|
|
if note.IDLength != 0 {
|
|
config.Note.IDOptions.Length = note.IDLength
|
|
}
|
|
if note.IDCharset != "" {
|
|
config.Note.IDOptions.Charset = charsetFromString(note.IDCharset)
|
|
}
|
|
if note.IDCase != "" {
|
|
config.Note.IDOptions.Case = caseFromString(note.IDCase)
|
|
}
|
|
if note.Lang != "" {
|
|
config.Note.Lang = note.Lang
|
|
}
|
|
if note.DefaultTitle != "" {
|
|
config.Note.DefaultTitle = note.DefaultTitle
|
|
}
|
|
for _, v := range note.Exclude {
|
|
config.Note.Exclude = append(config.Note.Exclude, v)
|
|
}
|
|
for _, v := range note.Ignore {
|
|
config.Note.Exclude = append(config.Note.Exclude, v)
|
|
}
|
|
if tomlConf.Extra != nil {
|
|
for k, v := range tomlConf.Extra {
|
|
config.Extra[k] = v
|
|
}
|
|
}
|
|
|
|
// Groups
|
|
for name, dirTOML := range tomlConf.Groups {
|
|
parent, ok := config.Groups[name]
|
|
if !ok {
|
|
parent = config.RootGroupConfig()
|
|
}
|
|
|
|
config.Groups[name] = parent.merge(dirTOML, name)
|
|
}
|
|
|
|
// Format
|
|
markdown := tomlConf.Format.Markdown
|
|
if markdown.Hashtags != nil {
|
|
config.Format.Markdown.Hashtags = *markdown.Hashtags
|
|
}
|
|
if markdown.ColonTags != nil {
|
|
config.Format.Markdown.ColonTags = *markdown.ColonTags
|
|
}
|
|
if markdown.MultiwordTags != nil {
|
|
config.Format.Markdown.MultiwordTags = *markdown.MultiwordTags
|
|
}
|
|
if markdown.LinkFormat != nil && *markdown.LinkFormat == "" {
|
|
*markdown.LinkFormat = "markdown"
|
|
}
|
|
if markdown.LinkFormat != nil {
|
|
config.Format.Markdown.LinkFormat = *markdown.LinkFormat
|
|
}
|
|
if markdown.LinkEncodePath != nil {
|
|
config.Format.Markdown.LinkEncodePath = *markdown.LinkEncodePath
|
|
} else if markdown.LinkFormat != nil {
|
|
config.Format.Markdown.LinkEncodePath = (*markdown.LinkFormat == "markdown")
|
|
}
|
|
if markdown.LinkDropExtension != nil {
|
|
config.Format.Markdown.LinkDropExtension = *markdown.LinkDropExtension
|
|
}
|
|
|
|
// Tool
|
|
tool := tomlConf.Tool
|
|
if tool.Editor != nil {
|
|
config.Tool.Editor = opt.NewNotEmptyString(*tool.Editor)
|
|
}
|
|
if tool.Shell != nil {
|
|
config.Tool.Shell = opt.NewNotEmptyString(*tool.Shell)
|
|
}
|
|
if tool.Pager != nil {
|
|
config.Tool.Pager = opt.NewStringWithPtr(tool.Pager)
|
|
}
|
|
if tool.FzfPreview != nil {
|
|
config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview)
|
|
}
|
|
if tool.FzfLine != nil {
|
|
config.Tool.FzfLine = opt.NewNotEmptyString(*tool.FzfLine)
|
|
}
|
|
if tool.FzfOptions != nil {
|
|
config.Tool.FzfOptions = opt.NewNotEmptyString(*tool.FzfOptions)
|
|
}
|
|
if tool.FzfBindNew != nil {
|
|
config.Tool.FzfBindNew = opt.NewStringWithPtr(tool.FzfBindNew)
|
|
}
|
|
|
|
// LSP completion
|
|
lspCompl := tomlConf.LSP.Completion
|
|
if lspCompl.NoteLabel != nil {
|
|
config.LSP.Completion.Note.Label = opt.NewNotEmptyString(*lspCompl.NoteLabel)
|
|
}
|
|
if lspCompl.NoteFilterText != nil {
|
|
config.LSP.Completion.Note.FilterText = opt.NewNotEmptyString(*lspCompl.NoteFilterText)
|
|
}
|
|
if lspCompl.NoteDetail != nil {
|
|
config.LSP.Completion.Note.Detail = opt.NewNotEmptyString(*lspCompl.NoteDetail)
|
|
}
|
|
config.LSP.Completion.UseAdditionalTextEdits = opt.NewBoolWithPtr(lspCompl.UseAdditionalTextEdits)
|
|
|
|
// LSP diagnostics
|
|
lspDiags := tomlConf.LSP.Diagnostics
|
|
if lspDiags.WikiTitle != nil {
|
|
config.LSP.Diagnostics.WikiTitle, err = lspDiagnosticSeverityFromString(*lspDiags.WikiTitle)
|
|
if err != nil {
|
|
return config, wrap(err)
|
|
}
|
|
}
|
|
if lspDiags.DeadLink != nil {
|
|
config.LSP.Diagnostics.DeadLink, err = lspDiagnosticSeverityFromString(*lspDiags.DeadLink)
|
|
if err != nil {
|
|
return config, wrap(err)
|
|
}
|
|
}
|
|
|
|
// Filters
|
|
if tomlConf.Filters != nil {
|
|
for k, v := range tomlConf.Filters {
|
|
config.Filters[k] = v
|
|
}
|
|
}
|
|
|
|
// Aliases
|
|
if tomlConf.Aliases != nil {
|
|
for k, v := range tomlConf.Aliases {
|
|
config.Aliases[k] = v
|
|
}
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig {
|
|
res := c.Clone()
|
|
|
|
if tomlConf.Paths != nil {
|
|
for _, p := range tomlConf.Paths {
|
|
res.Paths = append(res.Paths, p)
|
|
}
|
|
} else {
|
|
// If no `paths` config property was given for this group, we assume
|
|
// that its name will be used as the path.
|
|
res.Paths = append(res.Paths, name)
|
|
}
|
|
|
|
note := tomlConf.Note
|
|
if note.Filename != "" {
|
|
res.Note.FilenameTemplate = note.Filename
|
|
}
|
|
if note.Extension != "" {
|
|
res.Note.Extension = note.Extension
|
|
}
|
|
if note.Template != "" {
|
|
res.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template)
|
|
}
|
|
if note.IDLength != 0 {
|
|
res.Note.IDOptions.Length = note.IDLength
|
|
}
|
|
if note.IDCharset != "" {
|
|
res.Note.IDOptions.Charset = charsetFromString(note.IDCharset)
|
|
}
|
|
if note.IDCase != "" {
|
|
res.Note.IDOptions.Case = caseFromString(note.IDCase)
|
|
}
|
|
if note.Lang != "" {
|
|
res.Note.Lang = note.Lang
|
|
}
|
|
if note.DefaultTitle != "" {
|
|
res.Note.DefaultTitle = note.DefaultTitle
|
|
}
|
|
for _, v := range note.Exclude {
|
|
res.Note.Exclude = append(res.Note.Exclude, v)
|
|
}
|
|
for _, v := range note.Ignore {
|
|
res.Note.Exclude = append(res.Note.Exclude, v)
|
|
}
|
|
if tomlConf.Extra != nil {
|
|
for k, v := range tomlConf.Extra {
|
|
res.Extra[k] = v
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// tomlConfig holds the TOML representation of Config
|
|
type tomlConfig struct {
|
|
Notebook tomlNotebookConfig
|
|
Note tomlNoteConfig
|
|
Groups map[string]tomlGroupConfig `toml:"group"`
|
|
Format tomlFormatConfig
|
|
Tool tomlToolConfig
|
|
LSP tomlLSPConfig
|
|
Extra map[string]string
|
|
Filters map[string]string `toml:"filter"`
|
|
Aliases map[string]string `toml:"alias"`
|
|
}
|
|
|
|
type tomlNotebookConfig struct {
|
|
Dir string
|
|
}
|
|
|
|
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"`
|
|
Exclude []string `toml:"exclude"`
|
|
Ignore []string `toml:"ignore"` // Legacy alias to `exclude`
|
|
}
|
|
|
|
type tomlGroupConfig struct {
|
|
Paths []string
|
|
Note tomlNoteConfig
|
|
Extra map[string]string
|
|
}
|
|
|
|
type tomlFormatConfig struct {
|
|
Markdown tomlMarkdownConfig
|
|
}
|
|
|
|
type tomlMarkdownConfig struct {
|
|
Hashtags *bool `toml:"hashtags"`
|
|
ColonTags *bool `toml:"colon-tags"`
|
|
MultiwordTags *bool `toml:"multiword-tags"`
|
|
LinkFormat *string `toml:"link-format"`
|
|
LinkEncodePath *bool `toml:"link-encode-path"`
|
|
LinkDropExtension *bool `toml:"link-drop-extension"`
|
|
}
|
|
|
|
type tomlToolConfig struct {
|
|
Editor *string
|
|
Shell *string
|
|
Pager *string
|
|
FzfPreview *string `toml:"fzf-preview"`
|
|
FzfLine *string `toml:"fzf-line"`
|
|
FzfOptions *string `toml:"fzf-options"`
|
|
FzfBindNew *string `toml:"fzf-bind-new"`
|
|
}
|
|
|
|
type tomlLSPConfig struct {
|
|
Completion struct {
|
|
NoteLabel *string `toml:"note-label"`
|
|
NoteFilterText *string `toml:"note-filter-text"`
|
|
NoteDetail *string `toml:"note-detail"`
|
|
UseAdditionalTextEdits *bool `toml:"use-additional-text-edits"`
|
|
}
|
|
Diagnostics struct {
|
|
WikiTitle *string `toml:"wiki-title"`
|
|
DeadLink *string `toml:"dead-link"`
|
|
}
|
|
}
|
|
|
|
func charsetFromString(charset string) Charset {
|
|
switch charset {
|
|
case "alphanum":
|
|
return CharsetAlphanum
|
|
case "hex":
|
|
return CharsetHex
|
|
case "letters":
|
|
return CharsetLetters
|
|
case "numbers":
|
|
return CharsetNumbers
|
|
default:
|
|
return Charset(charset)
|
|
}
|
|
}
|
|
|
|
func caseFromString(c string) Case {
|
|
switch c {
|
|
case "lower":
|
|
return CaseLower
|
|
case "upper":
|
|
return CaseUpper
|
|
case "mixed":
|
|
return CaseMixed
|
|
default:
|
|
return CaseLower
|
|
}
|
|
}
|
|
|
|
func lspDiagnosticSeverityFromString(s string) (LSPDiagnosticSeverity, error) {
|
|
switch s {
|
|
case "", "none":
|
|
return LSPDiagnosticNone, nil
|
|
case "error":
|
|
return LSPDiagnosticError, nil
|
|
case "warning":
|
|
return LSPDiagnosticWarning, nil
|
|
case "info":
|
|
return LSPDiagnosticInfo, nil
|
|
case "hint":
|
|
return LSPDiagnosticHint, nil
|
|
default:
|
|
return LSPDiagnosticNone, fmt.Errorf("%s: unknown LSP diagnostic severity - may be none, hint, info, warning or error", s)
|
|
}
|
|
}
|