mirror of
https://github.com/mickael-menu/zk
synced 2024-11-07 15:20:21 +00:00
Support for VS Code and minor LSP fixes (#34)
This commit is contained in:
parent
b82b217078
commit
f3ebdb4813
@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
* An experimental Language Server for LSP-compatible editors:
|
||||
* Auto-complete Markdown links with `[[` (setup wiki-links in the [note formats configuration](docs/note-format.md))
|
||||
* Auto-complete [hashtags and colon-separated tags](docs/tags.md).
|
||||
* Preview the content of a note when hovering a link.
|
||||
* Navigate in your notes by following internal links.
|
||||
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
|
||||
* See [the documentation](docs/editors-integration.md) for configuration samples.
|
||||
* Pair `--match` with `--exact-match` / `-e` to search for (case insensitive) exact occurrences in your notes.
|
||||
* This can be useful when looking for terms including special characters, such as `[[name]]`.
|
||||
* Generating links to notes.
|
||||
|
15
Makefile
Normal file
15
Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
VERSION := `git describe --tags --match v[0-9]* 2> /dev/null`
|
||||
|
||||
all: macos linux
|
||||
rm -f zk
|
||||
|
||||
macos:
|
||||
rm -f zk && ./go build && zip -r "zk-${VERSION}-macos-`uname -m`.zip" zk
|
||||
|
||||
linux:
|
||||
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-i386 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-i386.tar.gz" zk
|
||||
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-amd64 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-amd64.tar.gz" zk
|
||||
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-arm64 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-arm64.tar.gz" zk
|
||||
|
||||
clean:
|
||||
rm -rf zk*
|
@ -13,7 +13,11 @@
|
||||
|
||||
* [Creating notes from templates](docs/note-creation.md)
|
||||
* [Advanced search and filtering capabilities](docs/note-filtering.md) including [tags](docs/tags.md), links and mentions
|
||||
* [Interactive browser](docs/tool-fzf), powered by `fzf`
|
||||
* [Integration with your favorite editors](docs/editors-integration.md):
|
||||
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
|
||||
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
|
||||
* [Any LSP-compatible editor](docs/editors-integration.md)
|
||||
* [Interactive browser](docs/tool-fzf.md), powered by `fzf`
|
||||
* [Git-style command aliases](docs/config-alias.md) and [named filters](docs/config-filter.md)
|
||||
* [Made with automation in mind](docs/automation.md)
|
||||
* [Notebook housekeeping](docs/notebook-housekeeping.md)
|
||||
|
86
docs/editors-integration.md
Normal file
86
docs/editors-integration.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Editors integration
|
||||
|
||||
There are several extensions available to integrate `zk` in your favorite editor:
|
||||
|
||||
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
|
||||
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
|
||||
|
||||
## Language Server Protocol
|
||||
|
||||
`zk` ships with a [Language Server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/) to provide basic support for any LSP-compatible editor. The currently supported features are:
|
||||
|
||||
* Auto-complete Markdown links with `[[` (setup wiki-links in the [note formats configuration](note-format.md))
|
||||
* Auto-complete [hashtags and colon-separated tags](tags.md).
|
||||
* Preview the content of a note when hovering a link.
|
||||
* Navigate in your notes by following internal links.
|
||||
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
|
||||
|
||||
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/mickael-menu/zk/issues/22).
|
||||
|
||||
### Vim and Neovim
|
||||
|
||||
#### Vim and Neovim 0.4
|
||||
|
||||
With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and add the following in the settings file:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Important, otherwise link completion containing spaces and other special characters won't work.
|
||||
"suggest.invalidInsertCharacters": [],
|
||||
|
||||
"languageserver": {
|
||||
"zk": {
|
||||
"command": "zk",
|
||||
"args": ["lsp"],
|
||||
"trace.server": "messages",
|
||||
"filetypes": ["markdown"]
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Neovim 0.5 built-in LSP client
|
||||
|
||||
Using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig):
|
||||
|
||||
```lua
|
||||
local lspconfig = require('lspconfig')
|
||||
local configs = require('lspconfig/configs')
|
||||
|
||||
configs.zk = {
|
||||
default_config = {
|
||||
cmd = {'zk', 'lsp'},
|
||||
filetypes = {'markdown'},
|
||||
root_dir = function()
|
||||
return vim.loop.cwd()
|
||||
end,
|
||||
settings = {}
|
||||
};
|
||||
}
|
||||
|
||||
lspconfig.zk.setup({ on_attach = function(client, buffer)
|
||||
-- Add keybindings here, see https://github.com/neovim/nvim-lspconfig#keybindings-and-completion
|
||||
end })
|
||||
```
|
||||
|
||||
### Sublime Text
|
||||
|
||||
Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run the **Preferences: LSP Settings** command. Add the following to the settings file:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"clients": {
|
||||
"zk": {
|
||||
"enabled": true,
|
||||
"command": ["zk", "lsp"],
|
||||
"languageId": "markdown",
|
||||
"scopes": [ "source.markdown" ],
|
||||
"syntaxes": [ "Packages/MarkdownEditing/Markdown.sublime-syntax" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
Install the [`zk-vscode`](https://marketplace.visualstudio.com/items?itemName=mickael-menu.zk-vscode) extension from the Marketplace.
|
2
go.mod
2
go.mod
@ -2,6 +2,8 @@ module github.com/mickael-menu/zk
|
||||
|
||||
go 1.15
|
||||
|
||||
replace github.com/tliron/glsp => github.com/mickael-menu/glsp v0.1.0
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.2.7
|
||||
github.com/alecthomas/kong v0.2.16-0.20210209082517-405b2f4fd9a4
|
||||
|
4
go.sum
4
go.sum
@ -369,6 +369,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mickael-menu/glsp v0.1.0 h1:we6mTssWXxGPVeEcTpCW8AOpdCuUXwUZ6Q2UiYVnCOw=
|
||||
github.com/mickael-menu/glsp v0.1.0/go.mod h1:ouzTGvQteTU4hdsG+32vIx0if7E9CzMa64d7tYJJ91g=
|
||||
github.com/mickael-menu/pretty v0.2.3 h1:AXi5WcBuWxwQV6iY/GhmCFpaoboQO2SLtzfujrn7dv0=
|
||||
github.com/mickael-menu/pretty v0.2.3/go.mod h1:gupeWUSWoo3KX7BItIuouLgTqQLlmRylpaPdIK6IqLk=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
@ -510,8 +512,6 @@ github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0
|
||||
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI=
|
||||
github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk=
|
||||
github.com/tliron/glsp v0.0.0-20210308190902-c7ec7df19257 h1:EIMeclnZjLgYIUs06pWOo+wIuOji9Q4Qz0MaU3va198=
|
||||
github.com/tliron/glsp v0.0.0-20210308190902-c7ec7df19257/go.mod h1:ouzTGvQteTU4hdsG+32vIx0if7E9CzMa64d7tYJJ91g=
|
||||
github.com/tliron/kutil v0.1.22 h1:VnwZ6YlTao2ISmm9wdv8CnVy5BjROBPpG65qIRc1LtE=
|
||||
github.com/tliron/kutil v0.1.22/go.mod h1:HkG4xQS2/BHI8EO9WfdOwnlUil7NhY/wmiV7U1uwEYw=
|
||||
github.com/tliron/yamlkeys v1.3.5/go.mod h1:8kJ1A/1s3p/I3MQUAbtv72dPEyQGoh0ZkQp0UAkABBo=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -84,6 +85,21 @@ func (d *document) LookBehind(pos protocol.Position, length int) string {
|
||||
return line[(charIdx - length):charIdx]
|
||||
}
|
||||
|
||||
// LookForward returns the n characters after the given position, on the same line.
|
||||
func (d *document) LookForward(pos protocol.Position, length int) string {
|
||||
line, ok := d.GetLine(int(pos.Line))
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
lineLength := len(line)
|
||||
charIdx := int(pos.Character)
|
||||
if lineLength <= charIdx+length {
|
||||
return line[charIdx:]
|
||||
}
|
||||
return line[charIdx:(charIdx + length)]
|
||||
}
|
||||
|
||||
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?:\|(.+?))?\]\]`)
|
||||
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
|
||||
|
||||
@ -134,6 +150,10 @@ func (d *document) DocumentLinks() ([]documentLink, error) {
|
||||
|
||||
for _, match := range markdownLinkRegex.FindAllStringSubmatchIndex(line, -1) {
|
||||
href := line[match[4]:match[5]]
|
||||
// Valid Markdown links are percent-encoded.
|
||||
if decodedHref, err := url.PathUnescape(href); err == nil {
|
||||
href = decodedHref
|
||||
}
|
||||
appendLink(href, match[0], match[1])
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,12 @@ func NewServer(opts ServerOpts) *Server {
|
||||
capabilities := handler.CreateServerCapabilities()
|
||||
capabilities.HoverProvider = true
|
||||
|
||||
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindIncremental
|
||||
change := protocol.TextDocumentSyncKindIncremental
|
||||
capabilities.TextDocumentSync = protocol.TextDocumentSyncOptions{
|
||||
OpenClose: boolPtr(true),
|
||||
Change: &change,
|
||||
Save: boolPtr(true),
|
||||
}
|
||||
capabilities.DocumentLinkProvider = &protocol.DocumentLinkOptions{
|
||||
ResolveProvider: boolPtr(true),
|
||||
}
|
||||
@ -94,6 +99,7 @@ func NewServer(opts ServerOpts) *Server {
|
||||
|
||||
capabilities.CompletionProvider = &protocol.CompletionOptions{
|
||||
TriggerCharacters: triggerChars,
|
||||
ResolveProvider: boolPtr(true),
|
||||
}
|
||||
|
||||
capabilities.DefinitionProvider = boolPtr(true)
|
||||
@ -201,6 +207,21 @@ func NewServer(opts ServerOpts) *Server {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
handler.CompletionItemResolve = func(context *glsp.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) {
|
||||
if path, ok := params.Data.(string); ok {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return params, err
|
||||
}
|
||||
params.Documentation = protocol.MarkupContent{
|
||||
Kind: protocol.MarkupKindMarkdown,
|
||||
Value: string(content),
|
||||
}
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
handler.TextDocumentHover = func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
if !ok {
|
||||
@ -377,41 +398,64 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notes, err := notebook.FindNotes(core.NoteFindOpts{})
|
||||
notes, err := notebook.FindMinimalNotes(core.NoteFindOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []protocol.CompletionItem
|
||||
for _, note := range notes {
|
||||
textEdit, err := s.buildTextEditForLink(notebook, note, doc, params.Position, linkFormatter)
|
||||
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter)
|
||||
if err != nil {
|
||||
s.logger.Err(errors.Wrapf(err, "failed to build TextEdit for note at %s", note.Path))
|
||||
s.logger.Err(err)
|
||||
continue
|
||||
}
|
||||
|
||||
label := note.Title
|
||||
if label == "" {
|
||||
label = note.Path
|
||||
}
|
||||
|
||||
items = append(items, protocol.CompletionItem{
|
||||
Label: label,
|
||||
TextEdit: textEdit,
|
||||
Documentation: protocol.MarkupContent{
|
||||
Kind: protocol.MarkupKindMarkdown,
|
||||
Value: note.RawContent,
|
||||
},
|
||||
})
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Server) buildTextEditForLink(notebook *core.Notebook, note core.ContextualNote, document *document, pos protocol.Position, linkFormatter core.LinkFormatter) (interface{}, error) {
|
||||
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
|
||||
item.Data = filepath.Join(notebook.Path, note.Path)
|
||||
|
||||
if note.Title != "" {
|
||||
item.Label = note.Title
|
||||
} else {
|
||||
item.Label = note.Path
|
||||
}
|
||||
|
||||
// Add the path to the filter text to be able to complete by it.
|
||||
item.FilterText = stringPtr(item.Label + " " + note.Path)
|
||||
|
||||
item.TextEdit, err = s.newTextEditForLink(notebook, note, doc, pos, linkFormatter)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to build TextEdit for note at %s", note.Path)
|
||||
return
|
||||
}
|
||||
|
||||
addTextEdits := []protocol.TextEdit{}
|
||||
|
||||
// Some LSP clients (e.g. VSCode) don't support deleting the trigger
|
||||
// characters with the main TextEdit. So let's add an additional
|
||||
// TextEdit for that.
|
||||
addTextEdits = append(addTextEdits, protocol.TextEdit{
|
||||
NewText: "",
|
||||
Range: rangeFromPosition(pos, -2, 0),
|
||||
})
|
||||
|
||||
item.AdditionalTextEdits = addTextEdits
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (interface{}, error) {
|
||||
path := filepath.Join(notebook.Path, note.Path)
|
||||
path = s.fs.Canonical(path)
|
||||
path, err := filepath.Rel(filepath.Dir(document.Path), path)
|
||||
path, err := filepath.Rel(filepath.Dir(doc.Path), path)
|
||||
if err != nil {
|
||||
path = note.Path
|
||||
}
|
||||
@ -421,16 +465,16 @@ func (s *Server) buildTextEditForLink(notebook *core.Notebook, note core.Context
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Overwrite [[ trigger
|
||||
start := pos
|
||||
start.Character -= 2
|
||||
// Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to
|
||||
// remove the closing ]] after the completion.
|
||||
endOffset := 0
|
||||
if doc.LookForward(pos, 2) == "]]" {
|
||||
endOffset = 2
|
||||
}
|
||||
|
||||
return protocol.TextEdit{
|
||||
Range: protocol.Range{
|
||||
Start: start,
|
||||
End: pos,
|
||||
},
|
||||
NewText: link,
|
||||
Range: rangeFromPosition(pos, 0, endOffset),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -440,6 +484,23 @@ func positionInRange(content string, rng protocol.Range, pos protocol.Position)
|
||||
return i >= start && i <= end
|
||||
}
|
||||
|
||||
func rangeFromPosition(pos protocol.Position, startOffset, endOffset int) protocol.Range {
|
||||
offsetPos := func(offset int) protocol.Position {
|
||||
newPos := pos
|
||||
if offset < 0 {
|
||||
newPos.Character -= uint32(-offset)
|
||||
} else {
|
||||
newPos.Character += uint32(offset)
|
||||
}
|
||||
return newPos
|
||||
}
|
||||
|
||||
return protocol.Range{
|
||||
Start: offsetPos(startOffset),
|
||||
End: offsetPos(endOffset),
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
b := v
|
||||
return &b
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
// LSP starts a server implementing the Language Server Protocol.
|
||||
type LSP struct {
|
||||
Log string `type:path placeholder:PATH help:"Absolute path to the log file"`
|
||||
Log string `hidden type:path placeholder:PATH help:"Absolute path to the log file"`
|
||||
}
|
||||
|
||||
func (cmd *LSP) Run(container *cli.Container) error {
|
||||
|
Loading…
Reference in New Issue
Block a user