diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e88f5b..bbb8443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. * 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). * Diagnostics to [report dead links or wiki-link titles](docs/config-lsp.md). + * Auto-complete only the path of a Markdown link by typing `[custom title]((`. * Customize the format of `fzf`'s lines [with your own template](docs/tool-fzf.md). ```toml [tool] diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go index 00b67c7..e07ec0c 100644 --- a/internal/adapter/lsp/server.go +++ b/internal/adapter/lsp/server.go @@ -91,7 +91,7 @@ func NewServer(opts ServerOpts) *Server { ResolveProvider: boolPtr(true), } - triggerChars := []string{"[", "#", ":"} + triggerChars := []string{"(", "[", "#", ":"} capabilities.ExecuteCommandProvider = &protocol.ExecuteCommandOptions{ Commands: []string{ @@ -184,6 +184,11 @@ func NewServer(opts ServerOpts) *Server { return nil, err } + switch doc.LookBehind(params.Position, 3) { + case "]((": + return server.buildLinkCompletionList(doc, notebook, params) + } + switch doc.LookBehind(params.Position, 2) { case "[[": return server.buildLinkCompletionList(doc, notebook, params) @@ -666,7 +671,7 @@ func (s *Server) buildInsertForTag(name string, triggerChar string, config core. } func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) ([]protocol.CompletionItem, error) { - linkFormatter, err := notebook.NewLinkFormatter() + linkFormatter, err := newLinkFormatter(doc, notebook, params) if err != nil { return nil, err } @@ -690,6 +695,14 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook, return items, nil } +func newLinkFormatter(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) (core.LinkFormatter, error) { + if doc.LookBehind(params.Position, 3) == "]((" { + return core.NewMarkdownLinkFormatter(notebook.Config.Format.Markdown, true) + } else { + return notebook.NewLinkFormatter() + } +} + func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (item protocol.CompletionItem, err error) { kind := protocol.CompletionItemKindReference item.Kind = &kind @@ -739,9 +752,10 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo } // Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to - // remove the closing ]] after the completion. + // remove the closing ]] or )) after the completion. endOffset := 0 - if doc.LookForward(pos, 2) == "]]" { + suffix := doc.LookForward(pos, 2) + if suffix == "]]" || suffix == "))" { endOffset = 2 } diff --git a/internal/core/link_format.go b/internal/core/link_format.go index 95fa485..edad8a9 100644 --- a/internal/core/link_format.go +++ b/internal/core/link_format.go @@ -15,47 +15,36 @@ type LinkFormatter func(path string, title string) (string, error) // NewLinkFormatter generates a new LinkFormatter from the user Markdown // configuration. func NewLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader) (LinkFormatter, error) { - var formatter LinkFormatter - var err error switch config.LinkFormat { case "markdown", "": - formatter, err = newMarkdownLinkFormatter(config) + return NewMarkdownLinkFormatter(config, false) case "wiki": - formatter, err = newWikiLinkFormatter(config) + return NewWikiLinkFormatter(config) default: - formatter, err = newCustomLinkFormatter(config, templateLoader) + return NewCustomLinkFormatter(config, templateLoader) } - - if err != nil { - return nil, err - } - - return func(path, title string) (string, error) { - if config.LinkDropExtension { - path = paths.DropExt(path) - } - if config.LinkEncodePath { - path = strings.ReplaceAll(url.PathEscape(path), "%2F", "/") - } - - return formatter(path, title) - }, nil } -func newMarkdownLinkFormatter(config MarkdownConfig) (LinkFormatter, error) { +func NewMarkdownLinkFormatter(config MarkdownConfig, onlyHref bool) (LinkFormatter, error) { return func(path, title string) (string, error) { + path = formatPath(path, config) if !config.LinkEncodePath { path = strings.ReplaceAll(path, `\`, `\\`) path = strings.ReplaceAll(path, `)`, `\)`) } - title = strings.ReplaceAll(title, `\`, `\\`) - title = strings.ReplaceAll(title, `]`, `\]`) - return fmt.Sprintf("[%s](%s)", title, path), nil + if onlyHref { + return fmt.Sprintf("(%s)", path), nil + } else { + title = strings.ReplaceAll(title, `\`, `\\`) + title = strings.ReplaceAll(title, `]`, `\]`) + return fmt.Sprintf("[%s](%s)", title, path), nil + } }, nil } -func newWikiLinkFormatter(config MarkdownConfig) (LinkFormatter, error) { +func NewWikiLinkFormatter(config MarkdownConfig) (LinkFormatter, error) { return func(path, title string) (string, error) { + path = formatPath(path, config) if !config.LinkEncodePath { path = strings.ReplaceAll(path, `\`, `\\`) path = strings.ReplaceAll(path, `]]`, `\]]`) @@ -64,7 +53,7 @@ func newWikiLinkFormatter(config MarkdownConfig) (LinkFormatter, error) { }, nil } -func newCustomLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader) (LinkFormatter, error) { +func NewCustomLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader) (LinkFormatter, error) { wrap := errors.Wrapperf("failed to render custom link with format: %s", config.LinkFormat) template, err := templateLoader.LoadTemplate(config.LinkFormat) if err != nil { @@ -72,6 +61,7 @@ func newCustomLinkFormatter(config MarkdownConfig, templateLoader TemplateLoader } return func(path, title string) (string, error) { + path = formatPath(path, config) return template.Render(customLinkRenderContext{Path: path, Title: title}) }, nil } @@ -80,3 +70,13 @@ type customLinkRenderContext struct { Path string Title string } + +func formatPath(path string, config MarkdownConfig) string { + if config.LinkDropExtension { + path = paths.DropExt(path) + } + if config.LinkEncodePath { + path = strings.ReplaceAll(url.PathEscape(path), "%2F", "/") + } + return path +} diff --git a/internal/core/link_format_test.go b/internal/core/link_format_test.go index 2f0f879..83c6dbc 100644 --- a/internal/core/link_format_test.go +++ b/internal/core/link_format_test.go @@ -36,6 +36,36 @@ func TestMarkdownLinkFormatter(t *testing.T) { test("path/to note.md", "An interesting subject", "[An interesting subject](path/to%20note)") } +func TestMarkdownLinkFormatterOnlyHref(t *testing.T) { + newTester := func(encodePath, dropExtension bool) func(path, expected string) { + formatter, err := NewMarkdownLinkFormatter(MarkdownConfig{ + LinkFormat: "markdown", + LinkEncodePath: encodePath, + LinkDropExtension: dropExtension, + }, true) + assert.Nil(t, err) + + return func(path, expected string) { + actual, err := formatter(path, "") + assert.Nil(t, err) + assert.Equal(t, actual, expected) + } + } + + test := newTester(false, false) + test("path/to note.md", "(path/to note.md)") + test("", "()") + test("path/to note.md", "(path/to note.md)") + test(`path/(no\te).md`, `(path/(no\\te\).md)`) + test = newTester(true, false) + test("path/to note.md", "(path/to%20note.md)") + test(`path/(no\te).md`, `(path/%28no%5Cte%29.md)`) + test = newTester(false, true) + test("path/to note.md", "(path/to note)") + test = newTester(true, true) + test("path/to note.md", "(path/to%20note)") +} + func TestWikiLinkFormatter(t *testing.T) { newTester := func(encodePath, dropExtension bool) func(path, title, expected string) { formatter, err := NewLinkFormatter(MarkdownConfig{