mirror of https://github.com/mickael-menu/zk
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
935 lines
24 KiB
Go
935 lines
24 KiB
Go
package lsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mickael-menu/zk/internal/core"
|
|
"github.com/mickael-menu/zk/internal/util"
|
|
dateutil "github.com/mickael-menu/zk/internal/util/date"
|
|
"github.com/mickael-menu/zk/internal/util/errors"
|
|
"github.com/mickael-menu/zk/internal/util/opt"
|
|
strutil "github.com/mickael-menu/zk/internal/util/strings"
|
|
"github.com/tliron/glsp"
|
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
|
glspserv "github.com/tliron/glsp/server"
|
|
"github.com/tliron/kutil/logging"
|
|
_ "github.com/tliron/kutil/logging/simple"
|
|
)
|
|
|
|
// Server holds the state of the Language Server.
|
|
type Server struct {
|
|
server *glspserv.Server
|
|
notebooks *core.NotebookStore
|
|
documents *documentStore
|
|
fs core.FileStorage
|
|
logger util.Logger
|
|
}
|
|
|
|
// ServerOpts holds the options to create a new Server.
|
|
type ServerOpts struct {
|
|
Name string
|
|
Version string
|
|
LogFile opt.String
|
|
Logger *util.ProxyLogger
|
|
Notebooks *core.NotebookStore
|
|
FS core.FileStorage
|
|
}
|
|
|
|
// NewServer creates a new Server instance.
|
|
func NewServer(opts ServerOpts) *Server {
|
|
fs := opts.FS
|
|
debug := !opts.LogFile.IsNull()
|
|
if debug {
|
|
logging.Configure(10, opts.LogFile.Value)
|
|
}
|
|
|
|
handler := protocol.Handler{}
|
|
glspServer := glspserv.NewServer(&handler, opts.Name, debug)
|
|
|
|
// Redirect zk's logger to GLSP's to avoid breaking the JSON-RPC protocol
|
|
// with unwanted output.
|
|
if opts.Logger != nil {
|
|
opts.Logger.Logger = newGlspLogger(glspServer.Log)
|
|
}
|
|
|
|
server := &Server{
|
|
server: glspServer,
|
|
notebooks: opts.Notebooks,
|
|
documents: newDocumentStore(fs, opts.Logger),
|
|
fs: fs,
|
|
logger: opts.Logger,
|
|
}
|
|
|
|
var clientCapabilities protocol.ClientCapabilities
|
|
|
|
handler.Initialize = func(context *glsp.Context, params *protocol.InitializeParams) (interface{}, error) {
|
|
clientCapabilities = params.Capabilities
|
|
|
|
// To see the logs with coc.nvim, run :CocCommand workspace.showOutput
|
|
// https://github.com/neoclide/coc.nvim/wiki/Debug-language-server#using-output-channel
|
|
if params.Trace != nil {
|
|
protocol.SetTraceValue(*params.Trace)
|
|
}
|
|
|
|
capabilities := handler.CreateServerCapabilities()
|
|
capabilities.HoverProvider = true
|
|
capabilities.DefinitionProvider = true
|
|
capabilities.CodeActionProvider = true
|
|
|
|
change := protocol.TextDocumentSyncKindIncremental
|
|
capabilities.TextDocumentSync = protocol.TextDocumentSyncOptions{
|
|
OpenClose: boolPtr(true),
|
|
Change: &change,
|
|
Save: boolPtr(true),
|
|
}
|
|
capabilities.DocumentLinkProvider = &protocol.DocumentLinkOptions{
|
|
ResolveProvider: boolPtr(true),
|
|
}
|
|
|
|
triggerChars := []string{"(", "[", "#", ":"}
|
|
|
|
capabilities.ExecuteCommandProvider = &protocol.ExecuteCommandOptions{
|
|
Commands: []string{
|
|
cmdIndex,
|
|
cmdNew,
|
|
},
|
|
}
|
|
capabilities.CompletionProvider = &protocol.CompletionOptions{
|
|
TriggerCharacters: triggerChars,
|
|
ResolveProvider: boolPtr(true),
|
|
}
|
|
|
|
capabilities.ReferencesProvider = &protocol.ReferenceOptions{}
|
|
|
|
return protocol.InitializeResult{
|
|
Capabilities: capabilities,
|
|
ServerInfo: &protocol.InitializeResultServerInfo{
|
|
Name: opts.Name,
|
|
Version: &opts.Version,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
handler.Initialized = func(context *glsp.Context, params *protocol.InitializedParams) error {
|
|
return nil
|
|
}
|
|
|
|
handler.Shutdown = func(context *glsp.Context) error {
|
|
protocol.SetTraceValue(protocol.TraceValueOff)
|
|
return nil
|
|
}
|
|
|
|
handler.SetTrace = func(context *glsp.Context, params *protocol.SetTraceParams) error {
|
|
protocol.SetTraceValue(params.Value)
|
|
return nil
|
|
}
|
|
|
|
handler.TextDocumentDidOpen = func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error {
|
|
doc, err := server.documents.DidOpen(*params, context.Notify)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if doc != nil {
|
|
server.refreshDiagnosticsOfDocument(doc, context.Notify, false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
handler.TextDocumentDidChange = func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
doc.ApplyChanges(params.ContentChanges)
|
|
server.refreshDiagnosticsOfDocument(doc, context.Notify, true)
|
|
return nil
|
|
}
|
|
|
|
handler.TextDocumentDidClose = func(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
|
server.documents.Close(params.TextDocument.URI)
|
|
return nil
|
|
}
|
|
|
|
handler.TextDocumentDidSave = func(context *glsp.Context, params *protocol.DidSaveTextDocumentParams) error {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
server.logger.Err(err)
|
|
return nil
|
|
}
|
|
|
|
_, err = notebook.Index(false)
|
|
server.logger.Err(err)
|
|
return nil
|
|
}
|
|
|
|
handler.TextDocumentCompletion = func(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) {
|
|
// We don't use the context because clients might not send it. Instead,
|
|
// we'll look for trigger patterns in the document.
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch doc.LookBehind(params.Position, 3) {
|
|
case "]((":
|
|
return server.buildLinkCompletionList(doc, notebook, params)
|
|
}
|
|
|
|
switch doc.LookBehind(params.Position, 2) {
|
|
case "[[":
|
|
return server.buildLinkCompletionList(doc, notebook, params)
|
|
}
|
|
|
|
switch doc.LookBehind(params.Position, 1) {
|
|
case "#":
|
|
if notebook.Config.Format.Markdown.Hashtags {
|
|
return server.buildTagCompletionList(notebook, "#")
|
|
}
|
|
case ":":
|
|
if notebook.Config.Format.Markdown.ColonTags {
|
|
return server.buildTagCompletionList(notebook, ":")
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
handler.CompletionItemResolve = func(context *glsp.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) {
|
|
if path, ok := params.Data.(string); ok {
|
|
content, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return params, err
|
|
}
|
|
params.Documentation = protocol.MarkupContent{
|
|
Kind: protocol.MarkupKindMarkdown,
|
|
Value: string(content),
|
|
}
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
handler.TextDocumentHover = func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
link, err := doc.DocumentLinkAt(params.Position)
|
|
if link == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
target, err := server.noteForLink(*link, doc, notebook)
|
|
if err != nil || target == nil {
|
|
return nil, err
|
|
}
|
|
|
|
path, err := uriToPath(target.URI)
|
|
if err != nil {
|
|
server.logger.Printf("unable to parse URI: %v", err)
|
|
return nil, err
|
|
}
|
|
path = fs.Canonical(path)
|
|
|
|
contents, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: protocol.MarkupKindMarkdown,
|
|
Value: string(contents),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
handler.TextDocumentDocumentLink = func(context *glsp.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
links, err := doc.DocumentLinks()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
documentLinks := []protocol.DocumentLink{}
|
|
for _, link := range links {
|
|
target, err := server.noteForLink(link, doc, notebook)
|
|
if target == nil || err != nil {
|
|
continue
|
|
}
|
|
|
|
documentLinks = append(documentLinks, protocol.DocumentLink{
|
|
Range: link.Range,
|
|
Target: &target.URI,
|
|
})
|
|
}
|
|
|
|
return documentLinks, err
|
|
}
|
|
|
|
handler.TextDocumentDefinition = func(context *glsp.Context, params *protocol.DefinitionParams) (interface{}, error) {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
link, err := doc.DocumentLinkAt(params.Position)
|
|
if link == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
target, err := server.noteForLink(*link, doc, notebook)
|
|
if link == nil || target == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// FIXME: Waiting for https://github.com/tliron/glsp/pull/3 to be
|
|
// merged before using LocationLink.
|
|
if false && isTrue(clientCapabilities.TextDocument.Definition.LinkSupport) {
|
|
return protocol.LocationLink{
|
|
OriginSelectionRange: &link.Range,
|
|
TargetURI: target.URI,
|
|
}, nil
|
|
} else {
|
|
return protocol.Location{
|
|
URI: target.URI,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
handler.WorkspaceExecuteCommand = func(context *glsp.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
|
|
switch params.Command {
|
|
case cmdIndex:
|
|
return server.executeCommandIndex(params.Arguments)
|
|
case cmdNew:
|
|
return server.executeCommandNew(context, params.Arguments)
|
|
default:
|
|
return nil, fmt.Errorf("unknown zk LSP command: %s", params.Command)
|
|
}
|
|
}
|
|
|
|
handler.TextDocumentCodeAction = func(context *glsp.Context, params *protocol.CodeActionParams) (interface{}, error) {
|
|
if isRangeEmpty(params.Range) {
|
|
return nil, nil
|
|
}
|
|
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
wd := filepath.Dir(doc.Path)
|
|
|
|
actions := []protocol.CodeAction{}
|
|
|
|
addAction := func(dir string, actionTitle string) error {
|
|
opts := cmdNewOpts{
|
|
Title: doc.ContentAtRange(params.Range),
|
|
Dir: dir,
|
|
InsertLinkAtLocation: &protocol.Location{
|
|
URI: params.TextDocument.URI,
|
|
Range: params.Range,
|
|
},
|
|
}
|
|
|
|
var jsonOpts map[string]interface{}
|
|
err := unmarshalJSON(opts, &jsonOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
actions = append(actions, protocol.CodeAction{
|
|
Title: actionTitle,
|
|
Kind: stringPtr(protocol.CodeActionKindRefactor),
|
|
Command: &protocol.Command{
|
|
Command: cmdNew,
|
|
Arguments: []interface{}{wd, jsonOpts},
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
addAction(wd, "New note in current directory")
|
|
addAction("", "New note in top directory")
|
|
|
|
return actions, nil
|
|
}
|
|
|
|
handler.TextDocumentReferences = func(context *glsp.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
|
|
doc, ok := server.documents.Get(params.TextDocument.URI)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
notebook, err := server.notebookOf(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
link, err := doc.DocumentLinkAt(params.Position)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if link == nil {
|
|
href, err := notebook.RelPath(doc.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
link = &documentLink{Href: href}
|
|
}
|
|
|
|
target, err := server.noteForLink(*link, doc, notebook)
|
|
if link == nil || target == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p, err := notebook.RelPath(target.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts := core.NoteFindOpts{
|
|
LinkTo: &core.LinkFilter{Paths: []string{p}},
|
|
}
|
|
|
|
notes, err := notebook.FindNotes(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var locations []protocol.Location
|
|
|
|
for _, note := range notes {
|
|
pos := strings.Index(note.RawContent, target.Path[0:len(target.Path)-3])
|
|
var line uint32 = 0
|
|
if pos < 0 {
|
|
line = 0
|
|
} else {
|
|
linePos := strings.Count(note.RawContent[0:pos], "\n")
|
|
line = uint32(linePos)
|
|
}
|
|
|
|
locations = append(locations, protocol.Location{
|
|
URI: pathToURI(filepath.Join(notebook.Path, note.Path)),
|
|
Range: protocol.Range{
|
|
Start: protocol.Position{
|
|
Line: line,
|
|
Character: 0,
|
|
},
|
|
End: protocol.Position{
|
|
Line: line,
|
|
Character: 0,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return locations, nil
|
|
}
|
|
|
|
return server
|
|
}
|
|
|
|
// Run starts the Language Server in stdio mode.
|
|
func (s *Server) Run() error {
|
|
return errors.Wrap(s.server.RunStdio(), "lsp")
|
|
}
|
|
|
|
const cmdIndex = "zk.index"
|
|
|
|
func (s *Server) executeCommandIndex(args []interface{}) (interface{}, error) {
|
|
if len(args) == 0 {
|
|
return nil, fmt.Errorf("zk.index expects a notebook path as first argument")
|
|
}
|
|
path, ok := args[0].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("zk.index expects a notebook path as first argument, got: %v", args[0])
|
|
}
|
|
|
|
force := false
|
|
if len(args) == 2 {
|
|
options, ok := args[1].(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("zk.index expects a dictionary of options as second argument, got: %v", args[1])
|
|
}
|
|
if forceOption, ok := options["force"]; ok {
|
|
force = toBool(forceOption)
|
|
}
|
|
}
|
|
|
|
notebook, err := s.notebooks.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return notebook.Index(force)
|
|
}
|
|
|
|
const cmdNew = "zk.new"
|
|
|
|
type cmdNewOpts struct {
|
|
Title string `json:"title,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
Dir string `json:"dir,omitempty"`
|
|
Group string `json:"group,omitempty"`
|
|
Template string `json:"template,omitempty"`
|
|
Extra map[string]string `json:"extra,omitempty"`
|
|
Date string `json:"date,omitempty"`
|
|
Edit jsonBoolean `json:"edit,omitempty"`
|
|
InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation,omitempty"`
|
|
}
|
|
|
|
func (s *Server) executeCommandNew(context *glsp.Context, args []interface{}) (interface{}, error) {
|
|
if len(args) == 0 {
|
|
return nil, fmt.Errorf("zk.index expects a notebook path as first argument")
|
|
}
|
|
wd, ok := args[0].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("zk.index expects a notebook path as first argument, got: %v", args[0])
|
|
}
|
|
|
|
var opts cmdNewOpts
|
|
if len(args) > 1 {
|
|
arg, ok := args[1].(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("zk.new expects a dictionary of options as second argument, got: %v", args[1])
|
|
}
|
|
err := unmarshalJSON(arg, &opts)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse zk.new args, got: %v", arg)
|
|
}
|
|
}
|
|
|
|
notebook, err := s.notebooks.Open(wd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
date, err := dateutil.TimeFromNatural(opts.Date)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "%s, failed to parse the `date` option", opts.Date)
|
|
}
|
|
|
|
note, err := notebook.NewNote(core.NewNoteOpts{
|
|
Title: opt.NewNotEmptyString(opts.Title),
|
|
Content: opts.Content,
|
|
Directory: opt.NewNotEmptyString(opts.Dir),
|
|
Group: opt.NewNotEmptyString(opts.Group),
|
|
Template: opt.NewNotEmptyString(opts.Template),
|
|
Extra: opts.Extra,
|
|
Date: date,
|
|
})
|
|
if err != nil {
|
|
var noteExists core.ErrNoteExists
|
|
if !errors.As(err, ¬eExists) {
|
|
return nil, err
|
|
}
|
|
note, err = notebook.FindNote(core.NoteFindOpts{
|
|
IncludePaths: []string{noteExists.Name},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if note == nil {
|
|
return nil, errors.New("zk.new could not generate a new note")
|
|
}
|
|
|
|
if opts.InsertLinkAtLocation != nil {
|
|
doc, ok := s.documents.Get(opts.InsertLinkAtLocation.URI)
|
|
if !ok {
|
|
return nil, fmt.Errorf("can't insert link in %s", opts.InsertLinkAtLocation.URI)
|
|
}
|
|
linkFormatter, err := notebook.NewLinkFormatter()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentDir := filepath.Dir(doc.Path)
|
|
linkFormatterContext, err := core.NewLinkFormatterContext(note.AsMinimalNote(), notebook.Path, currentDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
link, err := linkFormatter(linkFormatterContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
|
|
Edit: protocol.WorkspaceEdit{
|
|
Changes: map[string][]protocol.TextEdit{
|
|
opts.InsertLinkAtLocation.URI: {{Range: opts.InsertLinkAtLocation.Range, NewText: link}},
|
|
},
|
|
},
|
|
}, nil)
|
|
}
|
|
|
|
absPath := filepath.Join(notebook.Path, note.Path)
|
|
if opts.Edit {
|
|
go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
|
|
URI: pathToURI(absPath),
|
|
TakeFocus: boolPtr(true),
|
|
}, nil)
|
|
}
|
|
|
|
return map[string]interface{}{"path": absPath}, nil
|
|
}
|
|
|
|
func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
|
|
return s.notebooks.Open(doc.Path)
|
|
}
|
|
|
|
// noteForLink returns the LSP documentUri for the note targeted by the given link.
|
|
//
|
|
// Match by order of precedence:
|
|
// 1. Prefix of relative path
|
|
// 2. Find any occurrence of the href in a note path (substring)
|
|
// 3. Match the href as a term in the note titles
|
|
func (s *Server) noteForLink(link documentLink, doc *document, notebook *core.Notebook) (*Note, error) {
|
|
note, err := s.noteForHref(link.Href, doc, notebook)
|
|
if note == nil && err == nil && link.IsWikiLink {
|
|
// Try to find a partial href match.
|
|
note, err = notebook.FindByHref(link.Href, true)
|
|
if note == nil && err == nil {
|
|
// Fallback on matching the note title.
|
|
note, err = s.noteMatchingTitle(link.Href, notebook)
|
|
}
|
|
}
|
|
if note == nil || err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
joined_path := filepath.Join(notebook.Path, note.Path)
|
|
return &Note{*note, pathToURI(joined_path)}, nil
|
|
}
|
|
|
|
// noteForHref returns the LSP documentUri for the note targeted by the given HREF.
|
|
func (s *Server) noteForHref(href string, doc *document, notebook *core.Notebook) (*core.MinimalNote, error) {
|
|
if strutil.IsURL(href) {
|
|
return nil, nil
|
|
}
|
|
|
|
path := filepath.Clean(filepath.Join(filepath.Dir(doc.Path), href))
|
|
path, err := filepath.Rel(notebook.Path, path)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to resolve href: %s", href)
|
|
}
|
|
note, err := notebook.FindByHref(path, false)
|
|
if err != nil {
|
|
s.logger.Printf("findByHref(%s): %s", href, err.Error())
|
|
}
|
|
return note, err
|
|
}
|
|
|
|
// noteMatchingTitle returns the LSP documentUri for the note matching the given search terms.
|
|
func (s *Server) noteMatchingTitle(terms string, notebook *core.Notebook) (*core.MinimalNote, error) {
|
|
if terms == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
note, err := notebook.FindMatching("title:(" + terms + ")")
|
|
if err != nil {
|
|
s.logger.Printf("findMatching(title: %s): %s", terms, err.Error())
|
|
}
|
|
return note, err
|
|
}
|
|
|
|
type Note struct {
|
|
core.MinimalNote
|
|
URI protocol.DocumentUri
|
|
}
|
|
|
|
func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyFunc, delay bool) {
|
|
if doc.NeedsRefreshDiagnostics { // Already refreshing
|
|
return
|
|
}
|
|
|
|
notebook, err := s.notebookOf(doc)
|
|
if err != nil {
|
|
s.logger.Err(err)
|
|
return
|
|
}
|
|
|
|
diagConfig := notebook.Config.LSP.Diagnostics
|
|
if diagConfig.WikiTitle == core.LSPDiagnosticNone && diagConfig.DeadLink == core.LSPDiagnosticNone {
|
|
// No diagnostic enabled.
|
|
return
|
|
}
|
|
|
|
doc.NeedsRefreshDiagnostics = true
|
|
go func() {
|
|
if delay {
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
doc.NeedsRefreshDiagnostics = false
|
|
|
|
diagnostics := []protocol.Diagnostic{}
|
|
links, err := doc.DocumentLinks()
|
|
if err != nil {
|
|
s.logger.Err(err)
|
|
return
|
|
}
|
|
|
|
for _, link := range links {
|
|
if strutil.IsURL(link.Href) {
|
|
continue
|
|
}
|
|
target, err := s.noteForLink(link, doc, notebook)
|
|
if err != nil {
|
|
s.logger.Err(err)
|
|
continue
|
|
}
|
|
|
|
var severity protocol.DiagnosticSeverity
|
|
var message string
|
|
if target == nil {
|
|
if diagConfig.DeadLink == core.LSPDiagnosticNone {
|
|
continue
|
|
}
|
|
severity = protocol.DiagnosticSeverity(diagConfig.DeadLink)
|
|
message = "not found"
|
|
} else {
|
|
if link.HasTitle || diagConfig.WikiTitle == core.LSPDiagnosticNone {
|
|
continue
|
|
}
|
|
severity = protocol.DiagnosticSeverity(diagConfig.WikiTitle)
|
|
message = target.Title
|
|
}
|
|
|
|
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
Range: link.Range,
|
|
Severity: &severity,
|
|
Source: stringPtr("zk"),
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
go notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{
|
|
URI: doc.URI,
|
|
Diagnostics: diagnostics,
|
|
})
|
|
}()
|
|
}
|
|
|
|
func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar string) ([]protocol.CompletionItem, error) {
|
|
tags, err := notebook.FindCollections(core.CollectionKindTag)
|
|
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, notebook.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 core.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 (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) ([]protocol.CompletionItem, error) {
|
|
linkFormatter, err := newLinkFormatter(doc, notebook, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notes, err := notebook.FindMinimalNotes(core.NoteFindOpts{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var items []protocol.CompletionItem
|
|
for _, note := range notes {
|
|
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter)
|
|
if err != nil {
|
|
s.logger.Err(err)
|
|
continue
|
|
}
|
|
|
|
items = append(items, item)
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
func newLinkFormatter(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) (core.LinkFormatter, error) {
|
|
if doc.LookBehind(params.Position, 3) == "]((" {
|
|
return core.NewMarkdownLinkFormatter(notebook.Config.Format.Markdown, true)
|
|
} else {
|
|
return notebook.NewLinkFormatter()
|
|
}
|
|
}
|
|
|
|
func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (item protocol.CompletionItem, err error) {
|
|
kind := protocol.CompletionItemKindReference
|
|
item.Kind = &kind
|
|
item.Data = filepath.Join(notebook.Path, note.Path)
|
|
|
|
if note.Title != "" {
|
|
item.Label = note.Title
|
|
} else {
|
|
item.Label = note.Path
|
|
}
|
|
|
|
// Add the path to the filter text to be able to complete by it.
|
|
item.FilterText = stringPtr(item.Label + " " + note.Path)
|
|
|
|
item.TextEdit, err = s.newTextEditForLink(notebook, note, doc, pos, linkFormatter)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "failed to build TextEdit for note at %s", note.Path)
|
|
return
|
|
}
|
|
|
|
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),
|
|
})
|
|
|
|
item.AdditionalTextEdits = addTextEdits
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (interface{}, error) {
|
|
currentDir := filepath.Dir(doc.Path)
|
|
context, err := core.NewLinkFormatterContext(note, notebook.Path, currentDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
link, err := linkFormatter(context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to
|
|
// remove the closing ]] or )) after the completion.
|
|
endOffset := 0
|
|
suffix := doc.LookForward(pos, 2)
|
|
if suffix == "]]" || suffix == "))" {
|
|
endOffset = 2
|
|
}
|
|
|
|
return protocol.TextEdit{
|
|
NewText: link,
|
|
Range: rangeFromPosition(pos, 0, endOffset),
|
|
}, nil
|
|
}
|
|
|
|
func positionInRange(content string, rng protocol.Range, pos protocol.Position) bool {
|
|
start, end := rng.IndexesIn(content)
|
|
i := pos.IndexIn(content)
|
|
return i >= start && i <= end
|
|
}
|
|
|
|
func rangeFromPosition(pos protocol.Position, startOffset, endOffset int) protocol.Range {
|
|
offsetPos := func(offset int) protocol.Position {
|
|
newPos := pos
|
|
if offset < 0 {
|
|
newPos.Character -= uint32(-offset)
|
|
} else {
|
|
newPos.Character += uint32(offset)
|
|
}
|
|
return newPos
|
|
}
|
|
|
|
return protocol.Range{
|
|
Start: offsetPos(startOffset),
|
|
End: offsetPos(endOffset),
|
|
}
|
|
}
|
|
|
|
func isRangeEmpty(pos protocol.Range) bool {
|
|
return pos.Start == pos.End
|
|
}
|
|
|
|
func boolPtr(v bool) *bool {
|
|
b := v
|
|
return &b
|
|
}
|
|
|
|
func isTrue(v *bool) bool {
|
|
return v != nil && *v == true
|
|
}
|
|
|
|
func isFalse(v *bool) bool {
|
|
return v == nil || *v == false
|
|
}
|
|
|
|
func stringPtr(v string) *string {
|
|
s := v
|
|
return &s
|
|
}
|
|
|
|
func unmarshalJSON(obj interface{}, v interface{}) error {
|
|
js, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(js, v)
|
|
}
|
|
|
|
func toBool(obj interface{}) bool {
|
|
s := strings.ToLower(fmt.Sprint(obj))
|
|
return s == "true" || s == "1"
|
|
}
|