From 45abd401336eb55908f4883f98c1228a0181d91b Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Thu, 25 Apr 2019 16:08:20 +0000 Subject: [PATCH] internal/lsp: introduce snippet builder object *snippet.Builder helps you construct lsp completion snippet strings. It handles escaping for you, and it gives a convenient interface for writing nested placeholders. Note that the builder does not support snippet "variables" or associated features. They add a lot of complexity and don't seem very useful at this point (plus they aren't supported in many editors). Change-Id: I492ab2f6f0e08ed952154cbc0a17c86f32abf40a GitHub-Last-Rev: 35a3f5d1cd0b6fda81b2c942a02aa1bd25c90acd GitHub-Pull-Request: golang/tools#90 Reviewed-on: https://go-review.googlesource.com/c/tools/+/173661 Run-TryBot: Rebecca Stambler Reviewed-by: Rebecca Stambler --- internal/lsp/snippet/snippet_builder.go | 87 ++++++++++++++++++++ internal/lsp/snippet/snippet_builder_test.go | 49 +++++++++++ 2 files changed, 136 insertions(+) create mode 100644 internal/lsp/snippet/snippet_builder.go create mode 100644 internal/lsp/snippet/snippet_builder_test.go diff --git a/internal/lsp/snippet/snippet_builder.go b/internal/lsp/snippet/snippet_builder.go new file mode 100644 index 00000000..14c5d712 --- /dev/null +++ b/internal/lsp/snippet/snippet_builder.go @@ -0,0 +1,87 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package snippet implements the specification for the LSP snippet format. +// +// Snippets are "tab stop" templates returned as an optional attribute of LSP +// completion candidates. As the user presses tab, they cycle through a series of +// tab stops defined in the snippet. Each tab stop can optionally have placeholder +// text, which can be pre-selected by editors. For a full description of syntax +// and features, see "Snippet Syntax" at +// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion. +// +// A typical snippet looks like "foo(${1:i int}, ${2:s string})". +package snippet + +import ( + "fmt" + "strings" +) + +// A Builder is used to build an LSP snippet piecemeal. +// The zero value is ready to use. Do not copy a non-zero Builder. +type Builder struct { + // currentTabStop is the index of the previous tab stop. The + // next tab stop will be currentTabStop+1. + currentTabStop int + sb strings.Builder +} + +// Escape characters defined in https://microsoft.github.io/language-server-protocol/specification#textDocument_completion under "Grammar". +var replacer = strings.NewReplacer( + `\`, `\\`, + `}`, `\}`, + `$`, `\$`, +) + +func (b *Builder) WriteText(s string) { + replacer.WriteString(&b.sb, s) +} + +// WritePlaceholder writes a tab stop and placeholder value to the Builder. +// The callback style allows for creating nested placeholders. To write an +// empty tab stop, provide a nil callback. +func (b *Builder) WritePlaceholder(fn func(*Builder)) { + fmt.Fprintf(&b.sb, "${%d", b.nextTabStop()) + if fn != nil { + b.sb.WriteByte(':') + fn(b) + } + b.sb.WriteByte('}') +} + +// In addition to '\', '}', and '$', snippet choices also use '|' and ',' as +// meta characters, so they must be escaped within the choices. +var choiceReplacer = strings.NewReplacer( + `\`, `\\`, + `}`, `\}`, + `$`, `\$`, + `|`, `\|`, + `,`, `\,`, +) + +// WriteChoice writes a tab stop and list of text choices to the Builder. +// The user's editor will prompt the user to choose one of the choices. +func (b *Builder) WriteChoice(choices []string) { + fmt.Fprintf(&b.sb, "${%d|", b.nextTabStop()) + for i, c := range choices { + if i != 0 { + b.sb.WriteByte(',') + } + choiceReplacer.WriteString(&b.sb, c) + } + b.sb.WriteString("|}") +} + +// String returns the built snippet string. +func (b *Builder) String() string { + return b.sb.String() +} + +// nextTabStop returns the next tab stop index for a new placeholder. +func (b *Builder) nextTabStop() int { + // Tab stops start from 1, so increment before returning. + b.currentTabStop++ + return b.currentTabStop +} diff --git a/internal/lsp/snippet/snippet_builder_test.go b/internal/lsp/snippet/snippet_builder_test.go new file mode 100644 index 00000000..810a7fe1 --- /dev/null +++ b/internal/lsp/snippet/snippet_builder_test.go @@ -0,0 +1,49 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package snippet + +import ( + "testing" +) + +func TestSnippetBuilder(t *testing.T) { + expect := func(expected string, fn func(*Builder)) { + var b Builder + fn(&b) + if got := b.String(); got != expected { + t.Errorf("got %q, expected %q", got, expected) + } + } + + expect("", func(b *Builder) {}) + + expect(`hi { \} \$ | " , / \\`, func(b *Builder) { + b.WriteText(`hi { } $ | " , / \`) + }) + + expect("${1}", func(b *Builder) { + b.WritePlaceholder(nil) + }) + + expect("hi ${1:there}", func(b *Builder) { + b.WriteText("hi ") + b.WritePlaceholder(func(b *Builder) { + b.WriteText("there") + }) + }) + + expect(`${1:id=${2:{your id\}}}`, func(b *Builder) { + b.WritePlaceholder(func(b *Builder) { + b.WriteText("id=") + b.WritePlaceholder(func(b *Builder) { + b.WriteText("{your id}") + }) + }) + }) + + expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) { + b.WriteChoice([]string{"one", `{ } $ | " , / \`, "three"}) + }) +}