diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f8373..6bcb361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ All notable changes to this project will be documented in this file. ### Added -* LSP: `zk.new` now returns the created note's content in its output (`content`), and has two new options: - * `dryRun` will prevent `zk.new` from creating the note on the file system. - * `insertContentAtLocation` can be used to insert the created note's content into an arbitrary location. +* LSP: + * `zk.new` now returns the created note's content in its output (`content`), and has two new options: + * `dryRun` will prevent `zk.new` from creating the note on the file system. + * `insertContentAtLocation` can be used to insert the created note's content into an arbitrary location. + * A new `zk.link` command to insert a link to a given note (contributed by [@psanker](https://github.com/mickael-menu/zk/pull/284)). ## 0.12.0 diff --git a/docs/editors-integration.md b/docs/editors-integration.md index beb79de..95531e7 100644 --- a/docs/editors-integration.md +++ b/docs/editors-integration.md @@ -182,6 +182,18 @@ This LSP command calls `zk new` to create a new note. It can be useful to quickl * `path` containing the absolute path to the created note. * `content` containing the raw content of the created note. +#### `zk.link` + +This LSP command allows editors to tap into the note linking mechanism. It takes three arguments: + +1. A `path` to any file in the notebook that will be linked to +2. An LSP `location` object that points to where the link will be inserted +3. An optional title of the link. If `title` is not provided, the title of the note will be inserted instead + +`zk.link` returns a JSON object with the path to the linked note, if the linking was successful. + +**Note**: This command is _not_ exposed in the command line. This command is targeted at editor / plugin authors to extend zk functionality. + #### `zk.list` This LSP command calls `zk list` to search a notebook. It takes two arguments: diff --git a/internal/adapter/lsp/cmd_link.go b/internal/adapter/lsp/cmd_link.go new file mode 100755 index 0000000..b321037 --- /dev/null +++ b/internal/adapter/lsp/cmd_link.go @@ -0,0 +1,64 @@ +package lsp + +import ( + "fmt" + "path/filepath" + + "github.com/mickael-menu/zk/internal/core" + "github.com/mickael-menu/zk/internal/util/errors" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +const cmdLink = "zk.link" + +type cmdLinkOpts struct { + Path *string `json:"path"` + Location *protocol.Location `json:"location"` + Title *string `json:"title"` +} + +func executeCommandLink(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) { + var opts cmdLinkOpts + + if len(args) > 1 { + arg, ok := args[1].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdLink, args[1]) + } + err := unmarshalJSON(arg, &opts) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdLink, arg) + } + } + + if opts.Path == nil { + return nil, errors.New("'path' not provided") + } + + note, err := notebook.FindByHref(*opts.Path, false) + + if err != nil { + return nil, err + } + + if note == nil { + return nil, errors.New("Requested note to link to not found!") + } + + info := &linkInfo{ + note: note, + location: opts.Location, + title: opts.Title, + } + + err = linkNote(notebook, documents, context, info) + + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "path": filepath.Join(notebook.Path, note.Path), + }, nil +} diff --git a/internal/adapter/lsp/cmd_new.go b/internal/adapter/lsp/cmd_new.go old mode 100644 new mode 100755 index 66b3a3e..aa88c3d --- a/internal/adapter/lsp/cmd_new.go +++ b/internal/adapter/lsp/cmd_new.go @@ -83,37 +83,18 @@ func executeCommandNew(notebook *core.Notebook, documents *documentStore, contex } if !opts.DryRun && opts.InsertLinkAtLocation != nil { - doc, ok := 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 - } + minNote := note.AsMinimalNote() - path := core.NotebookPath{ - Path: note.Path, - BasePath: notebook.Path, - WorkingDir: filepath.Dir(doc.Path), - } - linkFormatterContext, err := core.NewLinkFormatterContext(path, note.Title, note.Metadata) - if err != nil { - return nil, err - } + info := &linkInfo{ + note: &minNote, + location: opts.InsertLinkAtLocation, + title: &opts.Title, + } + err := linkNote(notebook, documents, context, info) - link, err := linkFormatter(linkFormatterContext) - 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 err != nil { + return nil, err + } } absPath := filepath.Join(notebook.Path, note.Path) diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go index 75199c1..e5542ec 100644 --- a/internal/adapter/lsp/server.go +++ b/internal/adapter/lsp/server.go @@ -369,6 +369,13 @@ func NewServer(opts ServerOpts) *Server { } return executeCommandNew(nb, server.documents, context, params.Arguments) + case cmdLink: + nb, err := openNotebook() + if err != nil { + return nil, err + } + return executeCommandLink(nb, server.documents, context, params.Arguments) + case cmdList: nb, err := openNotebook() if err != nil { diff --git a/internal/adapter/lsp/util.go b/internal/adapter/lsp/util.go index e8b7d0f..f88b6fe 100644 --- a/internal/adapter/lsp/util.go +++ b/internal/adapter/lsp/util.go @@ -3,10 +3,14 @@ package lsp import ( "fmt" "net/url" + "path/filepath" "runtime" "strings" + "github.com/mickael-menu/zk/internal/core" "github.com/mickael-menu/zk/internal/util/errors" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" ) func pathToURI(path string) string { @@ -56,3 +60,59 @@ func (b *jsonBoolean) UnmarshalJSON(data []byte) error { } return nil } + +type linkInfo struct { + note *core.MinimalNote + location *protocol.Location + title *string +} + +func linkNote(notebook *core.Notebook, documents *documentStore, context *glsp.Context, info *linkInfo) error { + if info.location == nil { + return errors.New("'location' not provided") + } + + // Get current document to edit + doc, ok := documents.Get(info.location.URI) + if !ok { + return fmt.Errorf("Cannot insert link in '%s'", info.location.URI) + } + + formatter, err := notebook.NewLinkFormatter() + if err != nil { + return err + } + + path := core.NotebookPath{ + Path: info.note.Path, + BasePath: notebook.Path, + WorkingDir: filepath.Dir(doc.Path), + } + + var title *string + title = info.title + + if title == nil { + title = &info.note.Title + } + + formatterContext, err := core.NewLinkFormatterContext(path, *title, info.note.Metadata) + if err != nil { + return err + } + + link, err := formatter(formatterContext) + if err != nil { + return err + } + + go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{ + Edit: protocol.WorkspaceEdit{ + Changes: map[string][]protocol.TextEdit{ + info.location.URI: {{Range: info.location.Range, NewText: link}}, + }, + }, + }, nil) + + return nil +}