Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): add 'zk.link' LSP command #284

Merged
merged 5 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions docs/editors-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
64 changes: 64 additions & 0 deletions internal/adapter/lsp/cmd_link.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 10 additions & 29 deletions internal/adapter/lsp/cmd_new.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions internal/adapter/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
60 changes: 60 additions & 0 deletions internal/adapter/lsp/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}