Skip to content

Commit

Permalink
feat: Mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
opaoz committed Jun 16, 2024
1 parent 117cb7c commit 52b3d32
Show file tree
Hide file tree
Showing 18 changed files with 238 additions and 56 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ build: ## Build GO binary
@go build -o pug-lsp -ldflags="-s -w"

.PHONY: build

toc: ## Format README.md to add TOC
markdown-toc -i README.md

.PHONY: toc
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

An implementation of the Language Protocol Server for [Pug.js](https://pugjs.org)

<!-- toc -->

- [Features](#features)
* [Note](#note)
* [Tags suggestions](#tags-suggestions)
* [Attributes suggestions](#attributes-suggestions)
+ [Auto suggest common attributes (such as `style`, `class`, `title`) for tags](#auto-suggest-common-attributes-such-as-style-class-title-for-tags)
+ [Auto suggest events (such as `onclick`, `onenter`) for tags](#auto-suggest-events-such-as-onclick-onenter-for-tags)
+ [Auto suggest tag-specific attributes (such as `href` for `a`)](#auto-suggest-tag-specific-attributes-such-as-href-for-a)
+ [`&attributes` snippet](#attributes-snippet)
* [Mixins suggestions](#mixins-suggestions)
* [`Doctype` suggestions](#doctype-suggestions)
* [Keywords suggestions](#keywords-suggestions)
+ [`case .. when .. default`](#case--when--default)
+ [`if .. else`](#if--else)
- [Thanks](#thanks)

<!-- tocstop -->

## Features

`pug-lsp` aims to provide suggestions for you to edit `.pug` in your editor.
Expand Down Expand Up @@ -36,6 +55,13 @@ _Yes, it's [a real feature](https://pugjs.org/language/attributes.html#attribute

![attributes-shortcut](docs/attributes-shortcut.png)

### Mixins suggestions

Look through included files and suggest defined mixins!

![mixins](docs/mixins-suggestions.png)


### `Doctype` suggestions

PugJS has [a pre-defined list](https://pugjs.org/language/doctype.html) of possible doctypes.
Expand All @@ -57,9 +83,10 @@ _Note_ [Case Fall Through](https://pugjs.org/language/case.html#case-fall-throug
> The difference, however, is a fall through in JavaScript happens whenever a break statement is not explicitly included;
> in Pug, it only happens when a block is completely missing.
If you would like to not output anything in a specific case, add an explicit unbuffered break
If you would like to not output anything in a specific case, add an explicit unbuffered break snippet: `- break`
![break-suggestion](docs/break-snippet.png)


#### `if .. else`

![if-suggestion](docs/if-suggestions.png)
Expand Down
Binary file added docs/mixins-suggestions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pkg/completion/meta-params.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

type CompletionMetaParams struct {
DocumentStore *documents.DocumentStore
Doc *documents.Document
Params *protocol.CompletionParams
ExistingAttrs *query.ExistingAttributes
Expand Down
61 changes: 49 additions & 12 deletions pkg/completion/mixins.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
package completion

import (
"github.com/opa-oz/go-todo/todo"
"github.com/opa-oz/pug-lsp/pkg/documents"
"github.com/opa-oz/pug-lsp/pkg/query"
protocol "github.com/tliron/glsp/protocol_3_16"
)

func MixinsCompletion(_ *CompletionMetaParams, completionItems []protocol.CompletionItem) *[]protocol.CompletionItem {
valueKind := protocol.CompletionItemKindValue
func docMixins(doc *documents.Document, completionItems []protocol.CompletionItem, exclude string) *[]protocol.CompletionItem {
functionKind := protocol.CompletionItemKindFunction

todo.T("Grab mixins from lined files and offer")
defaultSymbol := "mixins"
defaultToInsert := "mixins."
for _, def := range doc.Mixins {
insert := def.Name
if insert == exclude {
continue
}

completionItems = append(completionItems, protocol.CompletionItem{
Label: todo.String("Look for mixins", "Default mixin - [Mixin]"),
Kind: &valueKind,
Detail: &defaultSymbol,
InsertText: &defaultToInsert,
})
if len(*def.Arguments) > 0 {
insert += "()"
}

completionItems = append(completionItems, protocol.CompletionItem{
Label: def.Name,
Kind: &functionKind,
Detail: &def.Definition,
InsertText: &insert,
})
}

return &completionItems
}

func MixinsCompletion(meta *CompletionMetaParams, completionItems []protocol.CompletionItem) *[]protocol.CompletionItem {
hasMixinDefAncestor := query.HasMixinDefinitionAncestor(meta.CurrentNode)
var excludeMixin string

if hasMixinDefAncestor {
mixinDef := query.FindUpwards(meta.CurrentNode, query.MixinDefinitionNode, -1)
if mixinDef != nil {
mixinName := query.FindDownwards(mixinDef, query.MixinNameNode, 2)
if mixinName != nil {
excludeMixin = (*meta.Doc.Content)[mixinName.StartByte():mixinName.EndByte()]
}
}
}

completionItems = *docMixins(meta.Doc, completionItems, excludeMixin)
for _, include := range meta.Doc.Includes {
doc, ok := meta.DocumentStore.Get(*include.URI)

if !ok {
(*meta.Logger).Println("Something shady with include", include.Path)
continue
}

completionItems = *docMixins(doc, completionItems, excludeMixin)
}

return &completionItems
}
28 changes: 16 additions & 12 deletions pkg/documents/document-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,24 @@ func (ds *DocumentStore) DocumentDidOpen(ctx context.Context, params protocol.Di
}

tree, err := pug.GetParsedTreeFromString(ctx, params.TextDocument.Text)

doc := &Document{
URI: uri,
Path: path,
Content: &params.TextDocument.Text,
Tree: tree,
Includes: make(map[string]*lsp.Include),
doc, ok := ds.Get(uri)
if ok {
doc.Tree = tree
doc.Content = &params.TextDocument.Text
ds.logger.Println("DidOpen already open document", uri)
} else {
doc = &Document{
URI: uri,
Path: path,
Content: &params.TextDocument.Text,
Tree: tree,
Includes: make(map[string]*lsp.Include),
Mixins: make(map[string]*lsp.Mixin),
}
}

doc.HasDoctype = query.FindDoctype(tree)
doc.RefreshMixins(ctx)
ds.documents[path] = doc

return doc, nil
Expand Down Expand Up @@ -88,13 +96,9 @@ func (ds *DocumentStore) LoadIncludedFile(ctx context.Context, include *lsp.Incl
return
}

uri := *include.Path
if !strings.HasPrefix(*include.Path, "file:/") {
uri = "file://" + *include.Path
}
params := protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: uri,
URI: *include.URI,
LanguageID: todo.String("Move to constant", "pug"),
Version: 1,
Text: string(content),
Expand Down
18 changes: 18 additions & 0 deletions pkg/documents/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Document struct {
Tree *sitter.Tree
Content *string
Includes map[string]*lsp.Include
Mixins map[string]*lsp.Mixin
HasDoctype bool
}

Expand All @@ -43,10 +44,27 @@ func (d *Document) ApplyChanges(ctx context.Context, changes []interface{}) erro
todo.T("Applied changes")
d.Tree = newTree
d.HasDoctype = query.FindDoctype(newTree)
d.RefreshMixins(ctx)

return nil
}

func (d *Document) RefreshMixins(ctx context.Context) {
d.Mixins = make(map[string]*lsp.Mixin)
mixinDefinitions := query.FindMixinDefinitions(d.Tree.RootNode())

if mixinDefinitions == nil {
return
}

for _, def := range *mixinDefinitions {
mixin := lsp.NewMixin(d.URI, def, d.Content)

if mixin != nil {
d.Mixins[mixin.Name] = mixin
}
}
}
func (d *Document) GetAtPosition(position *protocol.Position) *sitter.Node {
node := d.Tree.RootNode().NamedDescendantForPointRange(
sitter.Point{
Expand Down
1 change: 1 addition & 0 deletions pkg/html/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ var tagToAttributes = map[HtmlTag]*[]string{
Title: &allVisible,
Tr: &allVisible,
Html: &allVisible,
Ul: &allVisible,
}

func GetAttributes(tagName string) *[]string {
Expand Down
6 changes: 6 additions & 0 deletions pkg/lsp/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Include struct {
Original *string
Path *string
Prefix *string // This is prefix for suggestions - if you `include mixins/logo`, you should import it as `+mixins.logo`
URI *string
}

func NewInclude(original, path *string) *Include {
Expand All @@ -16,9 +17,14 @@ func NewInclude(original, path *string) *Include {
prefix = parts[len(parts)-2]
}

uri := *path
if !strings.HasPrefix(uri, "file:/") {
uri = "file://" + uri
}
return &Include{
Original: original,
Path: path,
Prefix: &prefix,
URI: &uri,
}
}
45 changes: 45 additions & 0 deletions pkg/lsp/mixin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package lsp

import (
"strings"

"github.com/opa-oz/pug-lsp/pkg/query"
sitter "github.com/smacker/go-tree-sitter"
)

type Mixin struct {
Source string
Name string
Definition string
Arguments *[]string
}

func NewMixin(source string, node *sitter.Node, content *string) *Mixin {
nameNode := query.FindDownwards(node, query.MixinNameNode, 2)

if nameNode == nil {
return nil
}

definition := (*content)[nameNode.StartByte():nameNode.EndByte()]
mixinAttributes := query.FindDownwards(node, query.MixinAttributesNode, 2)
var arguments []string

if mixinAttributes != nil {
attributesRanges, err := query.FindAll(mixinAttributes, query.AttributeNamesQ)
if err == nil {
for _, rng := range *attributesRanges {
arguments = append(arguments, strings.Trim((*content)[rng.StartPos:rng.EndPos], ""))
}

definition += (*content)[mixinAttributes.StartByte():mixinAttributes.EndByte()]
}
}

return &Mixin{
Source: source,
Name: (*content)[nameNode.StartByte():nameNode.EndByte()],
Arguments: &arguments,
Definition: definition,
}
}
42 changes: 27 additions & 15 deletions pkg/query/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,33 @@ import "github.com/opa-oz/go-todo/todo"
type NodeType string

const (
TagNode NodeType = "tag"
TagNameNode NodeType = "tag_name"
AttributeNode NodeType = "attribute"
AttributesNode NodeType = "attributes"
AttributeNameNode NodeType = "attribute_name"
AttributeValueNode NodeType = "attribute_value"
ChildrenNode NodeType = "children"
MixinNode NodeType = "mixin_use"
DoctypeNode NodeType = "doctype"
DoctypeNameNode NodeType = "doctype_name"
ContentNode NodeType = "content"
JSNode NodeType = "javascript"
BufferedCodeNode NodeType = "buffered_code"
UnBufferedCodeNode NodeType = "unbuffered_code"
CaseNode NodeType = "case"
TagNode NodeType = "tag"
TagNameNode NodeType = "tag_name"
AttributeNode NodeType = "attribute"
AttributesNode NodeType = "attributes"
AttributeNameNode NodeType = "attribute_name"
AttributeValueNode NodeType = "attribute_value"
ChildrenNode NodeType = "children"
MixinNode NodeType = "mixin_use"
DoctypeNode NodeType = "doctype"
DoctypeNameNode NodeType = "doctype_name"
ContentNode NodeType = "content"
JSNode NodeType = "javascript"
BufferedCodeNode NodeType = "buffered_code"
UnBufferedCodeNode NodeType = "unbuffered_code"
CaseNode NodeType = "case"
MixinDefinitionNode NodeType = "mixin_definition"
MixinNameNode NodeType = "mixin_name"
MixinAttributesNode NodeType = "mixin_attributes"
)

var MaxDepth = todo.Int("Is 5 enough?", 5)

type Query string

const (
AttributeNamesQ Query = "(attribute_name) @attr"
IncludeFilenamesQ Query = "(include (filename) @incl)"
DoctypeQ Query = "(doctype) @doc"
MixinDefinitionQ Query = "(mixin_definition) @def"
)
4 changes: 1 addition & 3 deletions pkg/query/find-all-includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
sitter "github.com/smacker/go-tree-sitter"
)

const query = "(include (filename) @incl)"

func FindAllIncludes(tree *sitter.Tree) (*[]*StrRange, error) {
return FindAll(tree.RootNode(), query)
return FindAll(tree.RootNode(), IncludeFilenamesQ)
}
20 changes: 18 additions & 2 deletions pkg/query/find-all.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,25 @@ type StrRange struct {
EndPos uint32
}

func FindAll(node *sitter.Node, query string) (*[]*StrRange, error) {
func FindAll(node *sitter.Node, query Query) (*[]*StrRange, error) {
var nodes []*StrRange

ns, err := FindAllNodes(node, query)

if err != nil {
return nil, err
}

for _, n := range *ns {
nodes = append(nodes, &StrRange{StartPos: n.StartByte(), EndPos: n.EndByte()})
}

return &nodes, nil
}

func FindAllNodes(node *sitter.Node, query Query) (*[]*sitter.Node, error) {
var nodes []*sitter.Node

q, err := sitter.NewQuery([]byte(query), pug.GetLanguage())
if err != nil {
return nil, err
Expand All @@ -28,7 +44,7 @@ func FindAll(node *sitter.Node, query string) (*[]*StrRange, error) {
}

for _, c := range m.Captures {
nodes = append(nodes, &StrRange{StartPos: c.Node.StartByte(), EndPos: c.Node.EndByte()})
nodes = append(nodes, c.Node)
}
}

Expand Down
Loading

0 comments on commit 52b3d32

Please sign in to comment.