Customize LSP completion items (#92)

pull/96/head
Mickaël Menu 3 years ago committed by GitHub
parent b1c69b4765
commit 439c1b1a69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,16 @@ All notable changes to this project will be documented in this file.
### Added
* New template variables `filename` and `filename-stem` when formatting notes (e.g. with `zk list --format`) and for the [`fzf-line`](docs/tool-fzf.md) config key.
* Customize how LSP completion items appear in your editor when auto-completing links with the [`[lsp.completion]` configuration section](docs/config-lsp.md).
```toml
[lsp.completion]
# Show the note title in the completion pop-up, or fallback on its path if empty.
note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
note-detail = "{{filename-stem}}"
```
### Fixed

@ -2,6 +2,32 @@
The `[lsp]` [configuration file](config.md) section provides settings to fine-tune the [LSP editors integration](editors-integration.md).
## Completion
Customize how completion items appear in your editor when auto-completing links with the `[lsp.completion]` sub-section.
| Setting | Type | Description |
|--------------------|------------|----------------------------------------------------------------------------|
| `note-label` | `template` | Label displayed in the completion pop-up for each note |
| `note-filter-text` | `template` | Text used as a source when filtering the completion pop-up with keystrokes |
| `note-detail` | `template` | Additional information about a completion item |
Each key accepts a [template](template.md) with the following context:
| Variable | Type | Description |
|-----------------|----------|--------------------------------------------------------------------|
| `filename` | string | Filename of the note, including its extension |
| `filename-stem` | string | Filename of the note without the file extension |
| `path` | string | File path to the note, relative to the notebook root |
| `abs-path` | string | Absolute file path to the note |
| `rel-path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `title-or-path` | string | Note title or path if empty |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>1</sup> |
1. YAML keys are normalized to lower case.
## Diagnostics
Use the `[lsp.diagnostics]` sub-section to configure how LSP diagnostics are reported to your editors. Each diagnostic setting can be:
@ -24,4 +50,12 @@ Use the `[lsp.diagnostics]` sub-section to configure how LSP diagnostics are rep
wiki-title = "hint"
# Warn for dead links between notes.
dead-link = "error"
```
[lsp.completion]
# Show the note title in the completion pop-up, or fallback on its path if empty.
note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
note-detail = "{{filename-stem}}"
```

@ -0,0 +1,66 @@
package lsp
import (
"path/filepath"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/paths"
)
// completionTemplates holds templates to render the various elements of an LSP
// completion item.
type completionTemplates struct {
Label core.Template
FilterText core.Template
Detail core.Template
}
func newCompletionTemplates(loader core.TemplateLoader, templates core.LSPCompletionTemplates) (result completionTemplates, err error) {
if !templates.Label.IsNull() {
result.Label, err = loader.LoadTemplate(*templates.Label.Value)
}
if !templates.FilterText.IsNull() {
result.FilterText, err = loader.LoadTemplate(*templates.FilterText.Value)
}
if !templates.Detail.IsNull() {
result.Detail, err = loader.LoadTemplate(*templates.Detail.Value)
}
return
}
type completionItemRenderContext struct {
ID int64
Filename string
FilenameStem string `handlebars:"filename-stem"`
Path string
AbsPath string `handlebars:"abs-path"`
RelPath string `handlebars:"rel-path"`
Title string
TitleOrPath string `handlebars:"title-or-path"`
Metadata map[string]interface{}
}
func newCompletionItemRenderContext(note core.MinimalNote, notebookDir string, currentDir string) (completionItemRenderContext, error) {
absPath := filepath.Join(notebookDir, note.Path)
relPath, err := filepath.Rel(currentDir, absPath)
if err != nil {
return completionItemRenderContext{}, err
}
context := completionItemRenderContext{
ID: int64(note.ID),
Filename: filepath.Base(note.Path),
FilenameStem: paths.FilenameStem(note.Path),
Path: note.Path,
AbsPath: absPath,
RelPath: relPath,
Title: note.Title,
TitleOrPath: note.Title,
Metadata: note.Metadata,
}
if context.TitleOrPath == "" {
context.TitleOrPath = note.Path
}
return context, nil
}

@ -23,21 +23,23 @@ import (
// Server holds the state of the Language Server.
type Server struct {
server *glspserv.Server
notebooks *core.NotebookStore
documents *documentStore
fs core.FileStorage
logger util.Logger
server *glspserv.Server
notebooks *core.NotebookStore
documents *documentStore
templateLoader core.TemplateLoader
fs core.FileStorage
logger util.Logger
}
// ServerOpts holds the options to create a new Server.
type ServerOpts struct {
Name string
Version string
LogFile opt.String
Logger *util.ProxyLogger
Notebooks *core.NotebookStore
FS core.FileStorage
Name string
Version string
LogFile opt.String
Logger *util.ProxyLogger
Notebooks *core.NotebookStore
TemplateLoader core.TemplateLoader
FS core.FileStorage
}
// NewServer creates a new Server instance.
@ -58,11 +60,12 @@ func NewServer(opts ServerOpts) *Server {
}
server := &Server{
server: glspServer,
notebooks: opts.Notebooks,
documents: newDocumentStore(fs, opts.Logger),
fs: fs,
logger: opts.Logger,
server: glspServer,
notebooks: opts.Notebooks,
documents: newDocumentStore(fs, opts.Logger),
templateLoader: opts.TemplateLoader,
fs: fs,
logger: opts.Logger,
}
var clientCapabilities protocol.ClientCapabilities
@ -788,6 +791,11 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
return nil, err
}
templates, err := newCompletionTemplates(s.templateLoader, notebook.Config.LSP.Completion.Note)
if err != nil {
return nil, err
}
notes, err := notebook.FindMinimalNotes(core.NoteFindOpts{})
if err != nil {
return nil, err
@ -795,7 +803,7 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
var items []protocol.CompletionItem
for _, note := range notes {
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter)
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter, templates)
if err != nil {
s.logger.Err(err)
continue
@ -815,24 +823,55 @@ func newLinkFormatter(doc *document, notebook *core.Notebook, params *protocol.C
}
}
func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (item protocol.CompletionItem, err error) {
func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter, templates completionTemplates) (protocol.CompletionItem, error) {
kind := protocol.CompletionItemKindReference
item.Kind = &kind
item.Data = filepath.Join(notebook.Path, note.Path)
item := protocol.CompletionItem{
Kind: &kind,
Data: filepath.Join(notebook.Path, note.Path),
}
if note.Title != "" {
item.Label = note.Title
templateContext, err := newCompletionItemRenderContext(note, notebook.Path, doc.Path)
if err != nil {
return item, err
}
if templates.Label != nil {
item.Label, err = templates.Label.Render(templateContext)
if err != nil {
return item, err
}
} else {
item.Label = note.Title
}
// Fallback on the note path to never have empty labels.
if item.Label == "" {
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)
if templates.FilterText != nil {
filterText, err := templates.FilterText.Render(templateContext)
if err != nil {
return item, err
}
item.FilterText = &filterText
}
if item.FilterText == nil || *item.FilterText == "" {
// Add the path to the filter text to be able to complete by it.
item.FilterText = stringPtr(item.Label + " " + note.Path)
}
if templates.Detail != nil {
detail, err := templates.Detail.Render(templateContext)
if err != nil {
return item, err
}
item.Detail = &detail
}
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
return item, err
}
addTextEdits := []protocol.TextEdit{}

@ -13,12 +13,13 @@ type LSP struct {
func (cmd *LSP) Run(container *cli.Container) error {
server := lsp.NewServer(lsp.ServerOpts{
Name: "zk",
Version: container.Version,
Logger: container.Logger,
LogFile: opt.NewNotEmptyString(cmd.Log),
Notebooks: container.Notebooks,
FS: container.FS,
Name: "zk",
Version: container.Version,
Logger: container.Logger,
LogFile: opt.NewNotEmptyString(cmd.Log),
Notebooks: container.Notebooks,
TemplateLoader: container.TemplateLoader,
FS: container.FS,
})
return server.Run()

@ -50,6 +50,13 @@ func NewDefaultConfig() Config {
},
},
LSP: LSPConfig{
Completion: LSPCompletionConfig{
Note: LSPCompletionTemplates{
Label: opt.NullString,
FilterText: opt.NullString,
Detail: opt.NullString,
},
},
Diagnostics: LSPDiagnosticConfig{
WikiTitle: LSPDiagnosticNone,
DeadLink: LSPDiagnosticError,
@ -148,9 +155,23 @@ type ToolConfig struct {
// LSPConfig holds the Language Server Protocol configuration.
type LSPConfig struct {
Completion LSPCompletionConfig
Diagnostics LSPDiagnosticConfig
}
// LSPCompletionConfig holds the LSP auto-completion configuration.
type LSPCompletionConfig struct {
Note LSPCompletionTemplates
}
// LSPCompletionConfig holds the LSP completion templates for a particular
// completion item type (e.g. note or tag).
type LSPCompletionTemplates struct {
Label opt.String
FilterText opt.String
Detail opt.String
}
// LSPDiagnosticConfig holds the LSP diagnostics configuration.
type LSPDiagnosticConfig struct {
WikiTitle LSPDiagnosticSeverity
@ -341,7 +362,19 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro
config.Tool.FzfLine = opt.NewNotEmptyString(*tool.FzfLine)
}
// LSP
// LSP completion
lspCompl := tomlConf.LSP.Completion
if lspCompl.NoteLabel != nil {
config.LSP.Completion.Note.Label = opt.NewNotEmptyString(*lspCompl.NoteLabel)
}
if lspCompl.NoteFilterText != nil {
config.LSP.Completion.Note.FilterText = opt.NewNotEmptyString(*lspCompl.NoteFilterText)
}
if lspCompl.NoteDetail != nil {
config.LSP.Completion.Note.Detail = opt.NewNotEmptyString(*lspCompl.NoteDetail)
}
// LSP diagnostics
lspDiags := tomlConf.LSP.Diagnostics
if lspDiags.WikiTitle != nil {
config.LSP.Diagnostics.WikiTitle, err = lspDiagnosticSeverityFromString(*lspDiags.WikiTitle)
@ -474,6 +507,11 @@ type tomlToolConfig struct {
}
type tomlLSPConfig struct {
Completion struct {
NoteLabel *string `toml:"note-label"`
NoteFilterText *string `toml:"note-filter-text"`
NoteDetail *string `toml:"note-detail"`
}
Diagnostics struct {
WikiTitle *string `toml:"wiki-title"`
DeadLink *string `toml:"dead-link"`

@ -124,6 +124,11 @@ func TestParseComplete(t *testing.T) {
[group."without path"]
paths = []
[lsp.completion]
note-label = "notelabel"
note-filter-text = "notefiltertext"
note-detail = "notedetail"
[lsp.diagnostics]
wiki-title = "hint"
@ -225,6 +230,13 @@ func TestParseComplete(t *testing.T) {
FzfLine: opt.NewString("{{title}}"),
},
LSP: LSPConfig{
Completion: LSPCompletionConfig{
Note: LSPCompletionTemplates{
Label: opt.NewString("notelabel"),
FilterText: opt.NewString("notefiltertext"),
Detail: opt.NewString("notedetail"),
},
},
Diagnostics: LSPDiagnosticConfig{
WikiTitle: LSPDiagnosticHint,
DeadLink: LSPDiagnosticNone,
@ -345,6 +357,13 @@ func TestParseMergesGroupConfig(t *testing.T) {
},
},
LSP: LSPConfig{
Completion: LSPCompletionConfig{
Note: LSPCompletionTemplates{
Label: opt.NullString,
FilterText: opt.NullString,
Detail: opt.NullString,
},
},
Diagnostics: LSPDiagnosticConfig{
WikiTitle: LSPDiagnosticNone,
DeadLink: LSPDiagnosticError,

@ -317,6 +317,16 @@ multiword-tags = false
# Warn for dead links between notes.
dead-link = "error"
[lsp.completion]
# Customize the completion pop-up of your LSP client.
# Show the note title in the completion pop-up, or fallback on its path if empty.
#note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
#note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
#note-detail = "{{filename-stem}}"
# NAMED FILTERS
#

Loading…
Cancel
Save