Support for VS Code and minor LSP fixes (#34)

This commit is contained in:
Mickaël Menu 2021-04-24 14:11:09 +02:00 committed by GitHub
parent b82b217078
commit f3ebdb4813
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 225 additions and 30 deletions

View File

@ -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
View 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*

View File

@ -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)

View 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
View File

@ -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
View File

@ -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=

View File

@ -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])
}

View File

@ -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

View File

@ -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 {