Skip to content

Commit

Permalink
feat: used snippets to auto-insert HTML tag ends, and templ expressio…
Browse files Browse the repository at this point in the history
…ns, contributes to #3
  • Loading branch information
a-h committed May 31, 2021
1 parent 5d9259a commit 15e40f0
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 133 deletions.
121 changes: 7 additions & 114 deletions cmd/lspcmd/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lspcmd
import (
"bytes"
"fmt"
"strings"
"sync"

"github.com/sourcegraph/go-lsp"
Expand All @@ -16,18 +15,13 @@ func newDocumentContents(logger *zap.Logger) *documentContents {
m: new(sync.Mutex),
uriToContents: make(map[string][]byte),
log: logger,
editors: []documentEditor{
autoInsertClosingTag,
autoInsertExpressionClose,
},
}
}

type documentContents struct {
m *sync.Mutex
uriToContents map[string][]byte
log *zap.Logger
editors []documentEditor
}

type documentEditor func(uri, prefix string, change lsp.TextDocumentContentChangeEvent) (requests []toClientRequest)
Expand Down Expand Up @@ -55,15 +49,15 @@ func (fc *documentContents) Delete(uri string) {
}

// Apply changes to the document from the client, and return a list of change requests to send back to the client.
func (fc *documentContents) Apply(uri string, changes []lsp.TextDocumentContentChangeEvent) (updated []byte, requestsToClient []toClientRequest, err error) {
func (fc *documentContents) Apply(uri string, changes []lsp.TextDocumentContentChangeEvent) (updated []byte, err error) {
fc.m.Lock()
defer fc.m.Unlock()
contents, ok := fc.uriToContents[uri]
if !ok {
err = fmt.Errorf("document not found")
return
}
updated, requestsToClient, err = fc.applyContentChanges(lsp.DocumentURI(uri), contents, changes)
updated, err = fc.applyContentChanges(lsp.DocumentURI(uri), contents, changes)
if err != nil {
return
}
Expand All @@ -75,15 +69,15 @@ func (fc *documentContents) Apply(uri string, changes []lsp.TextDocumentContentC
// It implements the ability to react to changes on document edits.
// MIT licensed.
// applyContentChanges updates `contents` based on `changes`
func (fc *documentContents) applyContentChanges(uri lsp.DocumentURI, contents []byte, changes []lsp.TextDocumentContentChangeEvent) (c []byte, toClientWorkspaceEdits []toClientRequest, err error) {
func (fc *documentContents) applyContentChanges(uri lsp.DocumentURI, contents []byte, changes []lsp.TextDocumentContentChangeEvent) (c []byte, err error) {
for _, change := range changes {
if change.Range == nil && change.RangeLength == 0 {
contents = []byte(change.Text) // new full content
continue
}
start, ok, why := offsetForPosition(contents, change.Range.Start)
if !ok {
return nil, toClientWorkspaceEdits, fmt.Errorf("received textDocument/didChange for invalid position %q on %q: %s", change.Range.Start, uri, why)
return nil, fmt.Errorf("received textDocument/didChange for invalid position %q on %q: %s", change.Range.Start, uri, why)
}
var end int
if change.RangeLength != 0 {
Expand All @@ -92,16 +86,11 @@ func (fc *documentContents) applyContentChanges(uri lsp.DocumentURI, contents []
// RangeLength not specified, work it out from Range.End
end, ok, why = offsetForPosition(contents, change.Range.End)
if !ok {
return nil, toClientWorkspaceEdits, fmt.Errorf("received textDocument/didChange for invalid position %q on %q: %s", change.Range.Start, uri, why)
return nil, fmt.Errorf("received textDocument/didChange for invalid position %q on %q: %s", change.Range.Start, uri, why)
}
}
if start < 0 || end > len(contents) || end < start {
return nil, toClientWorkspaceEdits, fmt.Errorf("received textDocument/didChange for out of range position %q on %q", change.Range, uri)
}
// Custom code to check for automatic text changes (insertion etc.).
for _, editor := range fc.editors {
editor := editor
toClientWorkspaceEdits = append(toClientWorkspaceEdits, editor(string(uri), string(contents[:start]), change)...)
return nil, fmt.Errorf("received textDocument/didChange for out of range position %q on %q", change.Range, uri)
}
// End of custom code.
// Try avoid doing too many allocations, so use bytes.Buffer
Expand All @@ -112,7 +101,7 @@ func (fc *documentContents) applyContentChanges(uri lsp.DocumentURI, contents []
b.Write(contents[end:])
contents = b.Bytes()
}
return contents, toClientWorkspaceEdits, nil
return contents, nil
}

func offsetForPosition(contents []byte, p lsp.Position) (offset int, valid bool, whyInvalid string) {
Expand Down Expand Up @@ -146,99 +135,3 @@ func offsetForPosition(contents []byte, p lsp.Position) (offset int, valid bool,
}

// end of content from SourceGraph.

// LSP text edit features for automatically inserting values.

// When you type "{% ", " %}" is inserted afterwards.
// When you type "{%= ", " %}" is inserted afterwards.
// When you type "{%! ", " %}" is inserted afterwards.
func autoInsertExpressionClose(uri, prefix string, change lsp.TextDocumentContentChangeEvent) (requests []toClientRequest) {
if change.Text == "" {
// It's a deletion.
return
}
// Check the last couple of bytes for "{%= ", "{% " and "{%! ".
last := 4
if last > len(prefix) {
last = len(prefix)
}
upToCaret := prefix[len(prefix)-last:] + change.Text
if shouldInsert := strings.HasSuffix(upToCaret, "{% ") || strings.HasSuffix(upToCaret, "{%= ") || strings.HasSuffix(upToCaret, "{%! "); !shouldInsert {
return
}
requests = append(requests, createWorkspaceApplyEditInsert(uri, " %}\n", change.Range.End, insertAfter))
return
}

// When you type "{% templ ", "{% endtempl %}" is inserted on the next line.
func autoInsertClosingTag(uri, prefix string, change lsp.TextDocumentContentChangeEvent) (requests []toClientRequest) {
if change.Text == "" {
// It's a deletion.
return
}
last := 10
if last > len(prefix) {
last = len(prefix)
}
upToCaret := prefix[len(prefix)-last:] + change.Text
tags := []string{
"{% templ ",
"{% if ",
"{% for ",
"{% switch ",
"{% case ",
"{% default ",
}
var insertEnd string
for i := 0; i < len(tags); i++ {
tag := tags[i]
if strings.HasSuffix(upToCaret, tag) {
insertEnd = tag[3 : len(tag)-1]
break
}
}
if insertEnd == "" {
return
}
insertAt := lsp.Position{
Line: change.Range.End.Line + 1,
Character: 0,
}
requests = append(requests, createWorkspaceApplyEditInsert(uri, "{% end"+insertEnd+" %}\n", insertAt, insertAfter))
return
}

type insertPosition int

const (
insertBefore insertPosition = iota
insertAfter
)

func createWorkspaceApplyEditInsert(documentURI, text string, at lsp.Position, position insertPosition) toClientRequest {
textRange := lsp.Range{
Start: at,
End: at,
}
if position == insertAfter {
textRange.Start.Character++
textRange.End.Character += len(text) + 1
}
return toClientRequest{
Method: "workspace/applyEdit",
Notif: false,
Params: applyWorkspaceEditParams{
Label: "templ close tag",
Edit: lsp.WorkspaceEdit{
Changes: map[string][]lsp.TextEdit{
documentURI: {
{
Range: textRange,
NewText: text,
},
},
},
},
},
}
}
43 changes: 24 additions & 19 deletions cmd/lspcmd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ func (p *Proxy) proxyInitialize(ctx context.Context, conn *jsonrpc2.Conn, req *j
if err != nil {
p.log.Error("proxyInitialize: client -> gopls: error sending request", zap.Error(err))
}
// Add the '<' and '{' trigger so that we can do snippets for tags.
resp.Capabilities.CompletionProvider.TriggerCharacters = append(resp.Capabilities.CompletionProvider.TriggerCharacters, "{", "<")
// Remove all the gopls commands.
resp.Capabilities.ExecuteCommandProvider.Commands = []string{}
// Reply to the client.
Expand All @@ -248,21 +250,28 @@ func (p *Proxy) proxyCompletion(ctx context.Context, conn *jsonrpc2.Conn, req *j
if err != nil {
p.log.Error("proxyCompletion: failed to unmarshal request params", zap.Error(err))
}
// Rewrite the request.
err = p.rewriteCompletionRequest(&params)
if err != nil {
p.log.Error("proxyCompletion: error rewriting request", zap.Error(err))
}
// Call gopls and get the response.
var resp lsp.CompletionList
err = p.gopls.Call(ctx, req.Method, &params, &resp)
if err != nil {
p.log.Error("proxyCompletion: client -> gopls: error sending request", zap.Error(err))
}
// Rewrite the response.
err = p.rewriteCompletionResponse(string(params.TextDocument.URI), &resp)
if err != nil {
p.log.Error("proxyCompletion: error rewriting response", zap.Error(err))
switch params.Context.TriggerCharacter {
case "<":
resp.Items = htmlSnippets
case "{":
resp.Items = templateSnippets
default:
// Rewrite the request.
err = p.rewriteCompletionRequest(&params)
if err != nil {
p.log.Error("proxyCompletion: error rewriting request", zap.Error(err))
}
// Call gopls and get the response.
err = p.gopls.Call(ctx, req.Method, &params, &resp)
if err != nil {
p.log.Error("proxyCompletion: client -> gopls: error sending request", zap.Error(err))
}
// Rewrite the response.
err = p.rewriteCompletionResponse(string(params.TextDocument.URI), &resp)
if err != nil {
p.log.Error("proxyCompletion: error rewriting response", zap.Error(err))
}
}
// Reply to the client.
err = conn.Reply(ctx, req.ID, &resp)
Expand Down Expand Up @@ -429,14 +438,10 @@ func (p *Proxy) rewriteDidChangeRequest(ctx context.Context, r *jsonrpc2.Request
return
}
// Apply content changes to the cached template.
templateText, requestsToClient, err := p.documentContents.Apply(string(params.TextDocument.URI), params.ContentChanges)
templateText, err := p.documentContents.Apply(string(params.TextDocument.URI), params.ContentChanges)
if err != nil {
return
}
// Apply changes to the client.
for i := 0; i < len(requestsToClient); i++ {
p.toClient <- requestsToClient[i]
}
// Update the Go code.
template, err := templ.ParseString(string(templateText))
if err != nil {
Expand Down
69 changes: 69 additions & 0 deletions cmd/lspcmd/snippets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package lspcmd

import "github.com/sourcegraph/go-lsp"

var htmlSnippets = []lsp.CompletionItem{
{
Label: "a",
InsertText: `a href="${1:}">{%= ${2:""} %}</a>`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "div",
InsertText: `div>
${0}
</div>`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
}

var templateSnippets = []lsp.CompletionItem{
{
Label: "%= string",
InsertText: `= ${1:string} %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "%! template",
InsertText: `! ${1:template} %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "% templ",
InsertText: `% templ ${1:name}(${2}) %}
$0
{% endtempl %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "% if",
InsertText: `% if ${1:true} %}
$0
{% endif %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "% for",
InsertText: ` for ${1} %}
$0
{% endfor %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
{
Label: "% switch",
InsertText: ` switch ${1} %}
case ${2}:
$0
{% endcase %}
{% endswitch %}`,
Kind: lsp.CompletionItemKind(lsp.CIKSnippet),
InsertTextFormat: lsp.ITFSnippet,
},
}

0 comments on commit 15e40f0

Please sign in to comment.