From 7b92ca06cc61210184a044cedb3b62c98fb9aae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Sun, 23 Jan 2022 13:43:02 +0100 Subject: [PATCH] Disable `additionalTextEdits` for completion items by default (#160) --- CHANGELOG.md | 2 + docs/config-lsp.md | 11 ++--- internal/adapter/lsp/server.go | 72 ++++++++++++++++++++++----------- internal/core/config.go | 11 +++-- internal/core/config_test.go | 2 + internal/util/opt/opt.go | 73 ++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51df79b..826b6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. * New `--date` flag for `zk new` to set the current date manually. * [#144](https://github.com/mickael-menu/zk/issues/144) LSP auto-completion of YAML frontmatter tags. +* [zk-nvim#26](https://github.com/mickael-menu/zk-nvim/issues/26) The LSP server doesn't use `additionalTextEdits` anymore to remove the trigger characters when completing links. + * You can customize the default behavior with the [`use-additional-text-edits` configuration key](docs/config-lsp.md). ### Fixed diff --git a/docs/config-lsp.md b/docs/config-lsp.md index ef4acd8..fe3371f 100644 --- a/docs/config-lsp.md +++ b/docs/config-lsp.md @@ -6,11 +6,12 @@ The `[lsp]` [configuration file](config.md) section provides settings to fine-tu 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 | +| 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 | +| `use-additional-text-edits` | `boolean` | Indicates whether `additionalTextEdits` will be used to remove the trigger characters | Each key accepts a [template](template.md) with the following context: diff --git a/internal/adapter/lsp/server.go b/internal/adapter/lsp/server.go index 454da9c..4ab8197 100644 --- a/internal/adapter/lsp/server.go +++ b/internal/adapter/lsp/server.go @@ -22,13 +22,14 @@ import ( // Server holds the state of the Language Server. type Server struct { - server *glspserv.Server - notebooks *core.NotebookStore - documents *documentStore - noteContentParser core.NoteContentParser - templateLoader core.TemplateLoader - fs core.FileStorage - logger util.Logger + server *glspserv.Server + notebooks *core.NotebookStore + documents *documentStore + noteContentParser core.NoteContentParser + templateLoader core.TemplateLoader + fs core.FileStorage + logger util.Logger + useAdditionalTextEdits opt.Bool } // ServerOpts holds the options to create a new Server. @@ -60,12 +61,13 @@ func NewServer(opts ServerOpts) *Server { } server := &Server{ - server: glspServer, - notebooks: opts.Notebooks, - documents: newDocumentStore(fs, opts.Logger), - templateLoader: opts.TemplateLoader, - fs: fs, - logger: opts.Logger, + server: glspServer, + notebooks: opts.Notebooks, + documents: newDocumentStore(fs, opts.Logger), + templateLoader: opts.TemplateLoader, + fs: fs, + logger: opts.Logger, + useAdditionalTextEdits: opt.NullBool, } var clientCapabilities protocol.ClientCapabilities @@ -79,6 +81,15 @@ func NewServer(opts ServerOpts) *Server { protocol.SetTraceValue(*params.Trace) } + if params.ClientInfo != nil { + if params.ClientInfo.Name == "Visual Studio Code" { + // Visual Studio Code doesn't seem to support inl + // VSCode doesn't support deleting the trigger characters with + // the main TextEdit. We'll use additional text edits instead. + server.useAdditionalTextEdits = opt.True + } + } + capabilities := handler.CreateServerCapabilities() capabilities.HoverProvider = true capabilities.DefinitionProvider = true @@ -792,17 +803,19 @@ func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNot return item, err } - addTextEdits := []protocol.TextEdit{} + if s.useAdditionalTextEditsWithNotebook(notebook) { + 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), - }) + // 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 + item.AdditionalTextEdits = addTextEdits + } return item, nil } @@ -822,6 +835,12 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo return nil, err } + // Overwrite [[ trigger directly if the additional text edits are disabled. + startOffset := 0 + if !s.useAdditionalTextEditsWithNotebook(notebook) { + startOffset = -2 + } + // Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to // remove the closing ]] or )) after the completion. endOffset := 0 @@ -832,10 +851,17 @@ func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNo return protocol.TextEdit{ NewText: link, - Range: rangeFromPosition(pos, 0, endOffset), + Range: rangeFromPosition(pos, startOffset, endOffset), }, nil } +func (s *Server) useAdditionalTextEditsWithNotebook(nb *core.Notebook) bool { + return nb.Config.LSP.Completion.UseAdditionalTextEdits. + Or(s.useAdditionalTextEdits). + OrBool(false). + Unwrap() +} + func positionInRange(content string, rng protocol.Range, pos protocol.Position) bool { start, end := rng.IndexesIn(content) i := pos.IndexIn(content) diff --git a/internal/core/config.go b/internal/core/config.go index 53f842c..b7d8ab4 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -161,7 +161,8 @@ type LSPConfig struct { // LSPCompletionConfig holds the LSP auto-completion configuration. type LSPCompletionConfig struct { - Note LSPCompletionTemplates + Note LSPCompletionTemplates + UseAdditionalTextEdits opt.Bool } // LSPCompletionConfig holds the LSP completion templates for a particular @@ -373,6 +374,7 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro if lspCompl.NoteDetail != nil { config.LSP.Completion.Note.Detail = opt.NewNotEmptyString(*lspCompl.NoteDetail) } + config.LSP.Completion.UseAdditionalTextEdits = opt.NewBoolWithPtr(lspCompl.UseAdditionalTextEdits) // LSP diagnostics lspDiags := tomlConf.LSP.Diagnostics @@ -508,9 +510,10 @@ type tomlToolConfig struct { type tomlLSPConfig struct { Completion struct { - NoteLabel *string `toml:"note-label"` - NoteFilterText *string `toml:"note-filter-text"` - NoteDetail *string `toml:"note-detail"` + NoteLabel *string `toml:"note-label"` + NoteFilterText *string `toml:"note-filter-text"` + NoteDetail *string `toml:"note-detail"` + UseAdditionalTextEdits *bool `toml:"use-additional-text-edits"` } Diagnostics struct { WikiTitle *string `toml:"wiki-title"` diff --git a/internal/core/config_test.go b/internal/core/config_test.go index 0cef227..9eaefee 100644 --- a/internal/core/config_test.go +++ b/internal/core/config_test.go @@ -126,6 +126,7 @@ func TestParseComplete(t *testing.T) { paths = [] [lsp.completion] + use-additional-text-edits = true note-label = "notelabel" note-filter-text = "notefiltertext" note-detail = "notedetail" @@ -236,6 +237,7 @@ func TestParseComplete(t *testing.T) { FilterText: opt.NewString("notefiltertext"), Detail: opt.NewString("notedetail"), }, + UseAdditionalTextEdits: opt.True, }, Diagnostics: LSPDiagnosticConfig{ WikiTitle: LSPDiagnosticHint, diff --git a/internal/util/opt/opt.go b/internal/util/opt/opt.go index 950885e..0a085d1 100644 --- a/internal/util/opt/opt.go +++ b/internal/util/opt/opt.go @@ -91,3 +91,76 @@ func (s String) String() string { func (s String) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%v"`, s)), nil } + +// Bool holds an optional boolean value. +type Bool struct { + Value *bool +} + +// NullBool represents an empty optional Bool. +var NullBool = Bool{nil} + +// True represents a true optional Bool. +var True = NewBool(true) + +// False represents a false optional Bool. +var False = NewBool(false) + +// NewBool creates a new optional Bool with the given value. +func NewBool(value bool) Bool { + return Bool{&value} +} + +// NewBool creates a new optional Bool with the given pointer. +// When nil, the Bool is considered null. +func NewBoolWithPtr(value *bool) Bool { + return Bool{value} +} + +// IsNull returns whether the optional Bool has no value. +func (s Bool) IsNull() bool { + return s.Value == nil +} + +// Or returns the receiver if it is not null, otherwise the given optional +// Bool. +func (s Bool) Or(other Bool) Bool { + if s.IsNull() { + return other + } else { + return s + } +} + +// OrBool returns the optional Bool value or the given default boolean if +// it is null. +func (s Bool) OrBool(alt bool) Bool { + if s.IsNull() { + return NewBool(alt) + } else { + return s + } +} + +// Unwrap returns the optional Bool value or false if none is set. +func (s Bool) Unwrap() bool { + if s.IsNull() { + return false + } else { + return *s.Value + } +} + +func (s Bool) Equal(other Bool) bool { + return s.Value == other.Value || + (s.Value != nil && other.Value != nil && *s.Value == *other.Value) +} + +func (s Bool) MarshalJSON() ([]byte, error) { + value := s.Unwrap() + if value { + return []byte("true"), nil + } else { + return []byte("false"), nil + } +}