Skip to content

Commit

Permalink
Auto complete tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu committed Mar 29, 2021
1 parent 39e49c5 commit 6a3da12
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 36 deletions.
2 changes: 1 addition & 1 deletion cmd/container.go → adapter/container.go
@@ -1,4 +1,4 @@
package cmd
package adapter

import (
"io"
Expand Down
151 changes: 130 additions & 21 deletions adapter/lsp/server.go
@@ -1,8 +1,16 @@
package lsp

import (
"fmt"
"strings"

"github.com/mickael-menu/zk/adapter"
"github.com/mickael-menu/zk/adapter/sqlite"
"github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/core/zk"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
strutil "github.com/mickael-menu/zk/util/strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
glspserv "github.com/tliron/glsp/server"
Expand All @@ -12,32 +20,46 @@ import (

// Server holds the state of the Language Server.
type Server struct {
server *glspserv.Server
initialized bool
clientCapabilities *protocol.ClientCapabilities
server *glspserv.Server
container *adapter.Container
}

// ServerOpts holds the options to create a new Server.
type ServerOpts struct {
Name string
Version string
LogFile opt.String
Name string
Version string
LogFile opt.String
Container *adapter.Container
}

// NewServer creates a new Server instance.
func NewServer(opts ServerOpts) *Server {
handler := protocol.Handler{}
debug := !opts.LogFile.IsNull()
server := &Server{
server: glspserv.NewServer(&handler, opts.Name, debug),
}

if debug {
logging.Configure(10, opts.LogFile.Value)
}

workspace := newWorkspace()
handler := protocol.Handler{}
server := &Server{
server: glspserv.NewServer(&handler, opts.Name, debug),
container: opts.Container,
}

handler.Initialize = func(context *glsp.Context, params *protocol.InitializeParams) (interface{}, error) {
server.clientCapabilities = &params.Capabilities
// clientCapabilities = &params.Capabilities

if len(params.WorkspaceFolders) > 0 {
for _, f := range params.WorkspaceFolders {
workspace.addFolder(f.URI)
}
} else if params.RootURI != nil {
workspace.addFolder(*params.RootURI)
} else if params.RootPath != nil {
workspace.addFolder(*params.RootPath)
}

server.container.OpenNotebook(workspace.folders)

// To see the logs with coc.nvim, run :CocCommand workspace.showOutput
// https://github.com/neoclide/coc.nvim/wiki/Debug-language-server#using-output-channel
Expand All @@ -46,13 +68,27 @@ func NewServer(opts ServerOpts) *Server {
}

capabilities := handler.CreateServerCapabilities()
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull
capabilities.DocumentLinkProvider = &protocol.DocumentLinkOptions{
ResolveProvider: boolPtr(true),
}
capabilities.CompletionProvider = &protocol.CompletionOptions{
ResolveProvider: boolPtr(true),
TriggerCharacters: []string{"#"},

zk, err := server.container.Zk()
if err == nil {
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull
capabilities.DocumentLinkProvider = &protocol.DocumentLinkOptions{
ResolveProvider: boolPtr(true),
}

triggerChars := []string{}

// Setup tag completion trigger characters
if zk.Config.Format.Markdown.Hashtags {
triggerChars = append(triggerChars, "#")
}
if zk.Config.Format.Markdown.ColonTags {
triggerChars = append(triggerChars, ":")
}

capabilities.CompletionProvider = &protocol.CompletionOptions{
TriggerCharacters: triggerChars,
}
}

return protocol.InitializeResult{
Expand All @@ -65,7 +101,6 @@ func NewServer(opts ServerOpts) *Server {
}

handler.Initialized = func(context *glsp.Context, params *protocol.InitializedParams) error {
server.initialized = true
return nil
}

Expand All @@ -79,7 +114,29 @@ func NewServer(opts ServerOpts) *Server {
return nil
}

// handler.TextDocumentCompletion = textDocumentCompletion
handler.WorkspaceDidChangeWorkspaceFolders = func(context *glsp.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
for _, f := range params.Event.Added {
workspace.addFolder(f.URI)
}
for _, f := range params.Event.Removed {
workspace.removeFolder(f.URI)
}
return nil
}

handler.TextDocumentCompletion = func(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) {
triggerChar := params.Context.TriggerCharacter
if params.Context.TriggerKind != protocol.CompletionTriggerKindTriggerCharacter || triggerChar == nil {
return nil, nil
}

switch *triggerChar {
case "#", ":":
return server.buildTagCompletionList(*triggerChar)
}

return nil, nil
}

return server
}
Expand All @@ -89,7 +146,59 @@ func (s *Server) Run() error {
return errors.Wrap(s.server.RunStdio(), "lsp")
}

func (s *Server) buildTagCompletionList(triggerChar string) ([]protocol.CompletionItem, error) {
zk, err := s.container.Zk()
if err != nil {
return nil, err
}
db, _, err := s.container.Database(false)
if err != nil {
return nil, err
}

var tags []note.Collection
err = db.WithTransaction(func(tx sqlite.Transaction) error {
tags, err = sqlite.NewCollectionDAO(tx, s.container.Logger).FindAll(note.CollectionKindTag)
return err
})
if err != nil {
return nil, err
}

var items []protocol.CompletionItem
for _, tag := range tags {
items = append(items, protocol.CompletionItem{
Label: tag.Name,
InsertText: s.buildInsertForTag(tag.Name, triggerChar, zk.Config),
Detail: stringPtr(fmt.Sprintf("%d %s", tag.NoteCount, strutil.Pluralize("note", tag.NoteCount))),
})
}

return items, nil
}

func (s *Server) buildInsertForTag(name string, triggerChar string, config zk.Config) *string {
switch triggerChar {
case ":":
name += ":"
case "#":
if strings.Contains(name, " ") {
if config.Format.Markdown.MultiwordTags {
name += "#"
} else {
name = strings.ReplaceAll(name, " ", "\\ ")
}
}
}
return &name
}

func boolPtr(v bool) *bool {
b := v
return &b
}

func stringPtr(v string) *string {
s := v
return &s
}
28 changes: 28 additions & 0 deletions adapter/lsp/workspace.go
@@ -0,0 +1,28 @@
package lsp

import "strings"

type workspace struct {
folders []string
}

func newWorkspace() *workspace {
return &workspace{
folders: []string{},
}
}

func (w *workspace) addFolder(folder string) {
folder = strings.TrimPrefix(folder, "file://")
w.folders = append(w.folders, folder)
}

func (w *workspace) removeFolder(folder string) {
folder = strings.TrimPrefix(folder, "file://")
for i, f := range w.folders {
if f == folder {
w.folders = append(w.folders[:i], w.folders[i+1:]...)
break
}
}
}
38 changes: 38 additions & 0 deletions adapter/sqlite/collection_dao.go
Expand Up @@ -18,6 +18,7 @@ type CollectionDAO struct {
// Prepared SQL statements
createCollectionStmt *LazyStmt
findCollectionStmt *LazyStmt
findAllCollectionsStmt *LazyStmt
findAssociationStmt *LazyStmt
createAssociationStmt *LazyStmt
removeAssociationsStmt *LazyStmt
Expand All @@ -42,6 +43,16 @@ func NewCollectionDAO(tx Transaction, logger util.Logger) *CollectionDAO {
WHERE kind = ? AND name = ?
`),

// Find all collections.
findAllCollectionsStmt: tx.PrepareLazy(`
SELECT c.name, COUNT(nc.id) as count
FROM collections c
LEFT JOIN notes_collections nc ON nc.collection_id = c.id
WHERE kind = ?
GROUP BY c.id
ORDER BY c.name
`),

// Returns whether a note and a collection are associated.
findAssociationStmt: tx.PrepareLazy(`
SELECT id FROM notes_collections
Expand Down Expand Up @@ -77,6 +88,33 @@ func (d *CollectionDAO) FindOrCreate(kind note.CollectionKind, name string) (cor
}
}

func (d *CollectionDAO) FindAll(kind note.CollectionKind) ([]note.Collection, error) {
rows, err := d.findAllCollectionsStmt.Query(kind)
if err != nil {
return []note.Collection{}, err
}
defer rows.Close()

collections := []note.Collection{}

for rows.Next() {
var name string
var count int
err := rows.Scan(&name, &count)
if err != nil {
return collections, err
}

collections = append(collections, note.Collection{
Kind: kind,
Name: name,
NoteCount: count,
})
}

return collections, nil
}

func (d *CollectionDAO) findCollection(kind note.CollectionKind, name string) (core.CollectionId, error) {
wrap := errors.Wrapperf("failed to get %s named %s", kind, name)

Expand Down
20 changes: 20 additions & 0 deletions adapter/sqlite/collection_dao_test.go
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/mickael-menu/zk/core"
"github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/util"
"github.com/mickael-menu/zk/util/test/assert"
)
Expand Down Expand Up @@ -32,6 +33,25 @@ func TestCollectionDAOFindOrCreate(t *testing.T) {
})
}

func TestCollectionDaoFindAll(t *testing.T) {

This comment has been minimized.

Copy link
@megalithic

megalithic Mar 30, 2021

so excited about this!!

testCollectionDAO(t, func(tx Transaction, dao *CollectionDAO) {
// Finds none
cs, err := dao.FindAll("missing")
assert.Nil(t, err)
assert.Equal(t, len(cs), 0)

// Finds existing
cs, err = dao.FindAll("tag")
assert.Nil(t, err)
assert.Equal(t, cs, []note.Collection{
{Kind: "tag", Name: "adventure", NoteCount: 2},
{Kind: "tag", Name: "fantasy", NoteCount: 1},
{Kind: "tag", Name: "fiction", NoteCount: 1},
{Kind: "tag", Name: "history", NoteCount: 1},
})
})
}

func TestCollectionDAOAssociate(t *testing.T) {
testCollectionDAO(t, func(tx Transaction, dao *CollectionDAO) {
// Returns existing association
Expand Down
3 changes: 2 additions & 1 deletion cmd/edit.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"

"github.com/mickael-menu/zk/adapter"
"github.com/mickael-menu/zk/adapter/fzf"
"github.com/mickael-menu/zk/adapter/sqlite"
"github.com/mickael-menu/zk/core/note"
Expand All @@ -17,7 +18,7 @@ type Edit struct {
Filtering
}

func (cmd *Edit) Run(container *Container) error {
func (cmd *Edit) Run(container *adapter.Container) error {
zk, err := container.Zk()
if err != nil {
return err
Expand Down
4 changes: 3 additions & 1 deletion cmd/index.go
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"fmt"

"github.com/mickael-menu/zk/adapter"
)

// Index indexes the content of all the notes in the notebook.
Expand All @@ -14,7 +16,7 @@ func (cmd *Index) Help() string {
return "You usually do not need to run `zk index` manually, as notes are indexed automatically when needed."
}

func (cmd *Index) Run(container *Container) error {
func (cmd *Index) Run(container *adapter.Container) error {
_, stats, err := container.Database(cmd.Force)
if err != nil {
return err
Expand Down
3 changes: 2 additions & 1 deletion cmd/list.go
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"

"github.com/mickael-menu/zk/adapter"
"github.com/mickael-menu/zk/adapter/fzf"
"github.com/mickael-menu/zk/adapter/sqlite"
"github.com/mickael-menu/zk/core/note"
Expand All @@ -22,7 +23,7 @@ type List struct {
Filtering
}

func (cmd *List) Run(container *Container) error {
func (cmd *List) Run(container *adapter.Container) error {
if cmd.Delimiter0 {
cmd.Delimiter = "\x00"
}
Expand Down

0 comments on commit 6a3da12

Please sign in to comment.