mirror of
https://github.com/mickael-menu/zk
synced 2024-11-07 15:20:21 +00:00
Add LSP custom commands and code actions to create new notes (#40)
This commit is contained in:
parent
3664734bda
commit
b17b42a06f
@ -6,12 +6,20 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
* [Editor integration through LSP](https://github.com/mickael-menu/zk/issues/22):
|
||||
* New code actions to create a note using the current selection as title.
|
||||
* Custom commands to [run `new` and `index` from your editor](docs/editors-integration.md#custom-commands).
|
||||
* Customize the format of `fzf`'s lines [with your own template](docs/tool-fzf.md).
|
||||
```toml
|
||||
[tool]
|
||||
fzf-line = "{{style 'green' path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
* Automatically index the notebook when saving a note with an LSP-enabled editor.
|
||||
* This ensures that tags and notes auto-completion lists are up-to-date.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Creating a new note from `fzf` in a directory containing spaces.
|
||||
|
@ -13,16 +13,21 @@ There are several extensions available to integrate `zk` in your favorite editor
|
||||
* Auto-complete [hashtags and colon-separated tags](tags.md).
|
||||
* Preview the content of a note when hovering a link.
|
||||
* Navigate in your notes by following internal links.
|
||||
* Create a new note using the current selection as title.
|
||||
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
|
||||
|
||||
### Editor LSP configurations
|
||||
|
||||
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/mickael-menu/zk/issues/22).
|
||||
|
||||
### Vim and Neovim
|
||||
#### Vim and Neovim
|
||||
|
||||
#### Vim and Neovim 0.4
|
||||
##### Vim and Neovim 0.4
|
||||
|
||||
With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and add the following in the settings file:
|
||||
|
||||
<details><summary><tt>coc-settings.json</tt></summary>
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Important, otherwise link completion containing spaces and other special characters won't work.
|
||||
@ -38,11 +43,39 @@ With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and ad
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
#### Neovim 0.5 built-in LSP client
|
||||
Here are some additional useful key bindings and custom commands:
|
||||
|
||||
<details><summary><tt>~/.config/nvim/init.vim</tt></summary>
|
||||
|
||||
```viml
|
||||
" User command to index the current notebook.
|
||||
"
|
||||
" zk.index expects a notebook path as first argument, so we provide the current
|
||||
" buffer path with expand("%:p").
|
||||
command! -nargs=0 ZkIndex :call CocAction("runCommand", "zk.index", expand("%:p"))
|
||||
nnoremap <leader>zi :ZkIndex<CR>
|
||||
|
||||
" User command to create and open a new note, to be called like this:
|
||||
" :ZkNew {"title": "An interesting subject", "dir": "inbox", ...}
|
||||
"
|
||||
" Note the concatenation with the "edit" command to open the note right away.
|
||||
command! -nargs=? ZkNew :exec "edit ".CocAction("runCommand", "zk.new", expand("%:p"), <args>).path
|
||||
|
||||
" Create a new note after prompting for its title.
|
||||
nnoremap <leader>zn :ZkNew {"title": input("Title: ")}<CR>
|
||||
" Create a new note in the directory journal/daily.
|
||||
nnoremap <leader>zj :ZkNew {"dir": "journal/daily"}<CR>
|
||||
```
|
||||
</details>
|
||||
|
||||
##### Neovim 0.5 built-in LSP client
|
||||
|
||||
Using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig):
|
||||
|
||||
<details><summary><tt>~/.config/nvim/init.lua</tt></summary>
|
||||
|
||||
```lua
|
||||
local lspconfig = require('lspconfig')
|
||||
local configs = require('lspconfig/configs')
|
||||
@ -62,11 +95,14 @@ lspconfig.zk.setup({ on_attach = function(client, buffer)
|
||||
-- Add keybindings here, see https://github.com/neovim/nvim-lspconfig#keybindings-and-completion
|
||||
end })
|
||||
```
|
||||
</details>
|
||||
|
||||
### Sublime Text
|
||||
#### Sublime Text
|
||||
|
||||
Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run the **Preferences: LSP Settings** command. Add the following to the settings file:
|
||||
|
||||
<details><summary><tt>LSP.sublime-settings</tt></summary>
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"clients": {
|
||||
@ -80,7 +116,60 @@ Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run t
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Visual Studio Code
|
||||
#### Visual Studio Code
|
||||
|
||||
Install the [`zk-vscode`](https://marketplace.visualstudio.com/items?itemName=mickael-menu.zk-vscode) extension from the Marketplace.
|
||||
|
||||
### Custom commands
|
||||
|
||||
Using `zk`'s LSP custom commands, you can call `zk` commands right from your editor. Please refer to your editor's documentation on how to bind keyboard shortcuts to custom LSP commands.
|
||||
|
||||
#### `zk.index`
|
||||
|
||||
This LSP command calls `zk index` to refresh your notebook's index. It can be useful to make sure that the auto-completion is up-to-date. `zk.index` takes two arguments:
|
||||
|
||||
1. A path to a file or directory in the notebook to index.
|
||||
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
|
||||
|
||||
| Key | Type | Description |
|
||||
|---------|---------|-----------------------------------|
|
||||
| `force` | boolean | Reindexes all the notes when true |
|
||||
</details>
|
||||
|
||||
`zk.index` returns a dictionary of indexing statistics.
|
||||
|
||||
#### `zk.new`
|
||||
|
||||
This LSP command calls `zk new` to create a new note. It can be useful to quickly create a new note with a key binding. `zk.new` takes two arguments:
|
||||
|
||||
1. A path to any file or directory in the notebook, to locate it.
|
||||
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
|
||||
|
||||
| Key | Type | Description |
|
||||
|------------------------|------------|-------------------------------------------------------------------------------------------|
|
||||
| `title` | string | Title of the new note |
|
||||
| `content` | string | Initial content of the note |
|
||||
| `dir` | string | Parent directory, relative to the root of the notebook |
|
||||
| `group` | string | [Note configuration group](config-group.md) |
|
||||
| `template` | string | [Custom template used to render the note](template-creation.md) |
|
||||
| `extra` | dictionary | A dictionary of extra variables to expand in the template |
|
||||
| `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" |
|
||||
| `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) |
|
||||
| `insertLinkAtLocation` | location | A location in another note where a link to the new note will be inserted |
|
||||
|
||||
The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"uri":"file:///Users/mickael/notes/9se3.md",
|
||||
"range": {
|
||||
"end":{"line": 5, "character":149},
|
||||
"start":{"line": 5, "character":137}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
`zk.new` returns a dictionary with the key `path` containing the absolute path to the newly created file.
|
||||
|
@ -5,15 +5,71 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mickael-menu/zk/internal/core"
|
||||
"github.com/mickael-menu/zk/internal/util"
|
||||
"github.com/mickael-menu/zk/internal/util/errors"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
"github.com/tliron/kutil/logging"
|
||||
)
|
||||
|
||||
// documentStore holds opened documents.
|
||||
type documentStore struct {
|
||||
documents map[string]*document
|
||||
fs core.FileStorage
|
||||
logger util.Logger
|
||||
}
|
||||
|
||||
func newDocumentStore(fs core.FileStorage, logger util.Logger) *documentStore {
|
||||
return &documentStore{
|
||||
documents: map[string]*document{},
|
||||
fs: fs,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *documentStore) DidOpen(params protocol.DidOpenTextDocumentParams) error {
|
||||
langID := params.TextDocument.LanguageID
|
||||
if langID != "markdown" && langID != "vimwiki" {
|
||||
return nil
|
||||
}
|
||||
|
||||
path, err := s.normalizePath(params.TextDocument.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.documents[path] = &document{
|
||||
Path: path,
|
||||
Content: params.TextDocument.Text,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *documentStore) Close(uri protocol.DocumentUri) {
|
||||
delete(s.documents, uri)
|
||||
}
|
||||
|
||||
func (s *documentStore) Get(pathOrURI string) (*document, bool) {
|
||||
path, err := s.normalizePath(pathOrURI)
|
||||
if err != nil {
|
||||
s.logger.Err(err)
|
||||
return nil, false
|
||||
}
|
||||
d, ok := s.documents[path]
|
||||
return d, ok
|
||||
}
|
||||
|
||||
func (s *documentStore) normalizePath(pathOrUri string) (string, error) {
|
||||
path, err := uriToPath(pathOrUri)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "unable to parse URI: %s", pathOrUri)
|
||||
}
|
||||
return s.fs.Canonical(path), nil
|
||||
}
|
||||
|
||||
// document represents an opened file.
|
||||
type document struct {
|
||||
Path string
|
||||
Content string
|
||||
Log logging.Logger
|
||||
lines []string
|
||||
}
|
||||
|
||||
@ -53,6 +109,12 @@ func (d *document) WordAt(pos protocol.Position) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ContentAtRange returns the document text at given range.
|
||||
func (d *document) ContentAtRange(rng protocol.Range) string {
|
||||
startIndex, endIndex := rng.IndexesIn(d.Content)
|
||||
return d.Content[startIndex:endIndex]
|
||||
}
|
||||
|
||||
// GetLine returns the line at the given index.
|
||||
func (d *document) GetLine(index int) (string, bool) {
|
||||
lines := d.GetLines()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
|
||||
"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"
|
||||
@ -22,7 +24,7 @@ import (
|
||||
type Server struct {
|
||||
server *glspserv.Server
|
||||
notebooks *core.NotebookStore
|
||||
documents map[protocol.DocumentUri]*document
|
||||
documents *documentStore
|
||||
fs core.FileStorage
|
||||
logger util.Logger
|
||||
}
|
||||
@ -45,20 +47,21 @@ func NewServer(opts ServerOpts) *Server {
|
||||
logging.Configure(10, opts.LogFile.Value)
|
||||
}
|
||||
|
||||
workspace := newWorkspace()
|
||||
handler := protocol.Handler{}
|
||||
server := &Server{
|
||||
server: glspserv.NewServer(&handler, opts.Name, debug),
|
||||
notebooks: opts.Notebooks,
|
||||
documents: map[string]*document{},
|
||||
fs: fs,
|
||||
}
|
||||
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(server.server.Log)
|
||||
server.logger = opts.Logger
|
||||
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
|
||||
@ -66,16 +69,6 @@ func NewServer(opts ServerOpts) *Server {
|
||||
handler.Initialize = func(context *glsp.Context, params *protocol.InitializeParams) (interface{}, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@ -84,6 +77,8 @@ func NewServer(opts ServerOpts) *Server {
|
||||
|
||||
capabilities := handler.CreateServerCapabilities()
|
||||
capabilities.HoverProvider = true
|
||||
capabilities.DefinitionProvider = true
|
||||
capabilities.CodeActionProvider = true
|
||||
|
||||
change := protocol.TextDocumentSyncKindIncremental
|
||||
capabilities.TextDocumentSync = protocol.TextDocumentSyncOptions{
|
||||
@ -97,13 +92,17 @@ func NewServer(opts ServerOpts) *Server {
|
||||
|
||||
triggerChars := []string{"[", "#", ":"}
|
||||
|
||||
capabilities.ExecuteCommandProvider = &protocol.ExecuteCommandOptions{
|
||||
Commands: []string{
|
||||
cmdIndex,
|
||||
cmdNew,
|
||||
},
|
||||
}
|
||||
capabilities.CompletionProvider = &protocol.CompletionOptions{
|
||||
TriggerCharacters: triggerChars,
|
||||
ResolveProvider: boolPtr(true),
|
||||
}
|
||||
|
||||
capabilities.DefinitionProvider = boolPtr(true)
|
||||
|
||||
return protocol.InitializeResult{
|
||||
Capabilities: capabilities,
|
||||
ServerInfo: &protocol.InitializeResultServerInfo{
|
||||
@ -127,40 +126,12 @@ func NewServer(opts ServerOpts) *Server {
|
||||
return nil
|
||||
}
|
||||
|
||||
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.TextDocumentDidOpen = func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error {
|
||||
langID := params.TextDocument.LanguageID
|
||||
if langID != "markdown" && langID != "vimwiki" {
|
||||
return nil
|
||||
}
|
||||
|
||||
path, err := uriToPath(params.TextDocument.URI)
|
||||
if err != nil {
|
||||
server.logger.Printf("unable to parse URI: %v", err)
|
||||
return nil
|
||||
}
|
||||
path = fs.Canonical(path)
|
||||
|
||||
server.documents[params.TextDocument.URI] = &document{
|
||||
Path: path,
|
||||
Content: params.TextDocument.Text,
|
||||
Log: server.server.Log,
|
||||
}
|
||||
|
||||
return nil
|
||||
return server.documents.DidOpen(*params)
|
||||
}
|
||||
|
||||
handler.TextDocumentDidChange = func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error {
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
doc, ok := server.documents.Get(params.TextDocument.URI)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@ -170,11 +141,24 @@ func NewServer(opts ServerOpts) *Server {
|
||||
}
|
||||
|
||||
handler.TextDocumentDidClose = func(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||
delete(server.documents, params.TextDocument.URI)
|
||||
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
|
||||
}
|
||||
|
||||
@ -184,7 +168,7 @@ func NewServer(opts ServerOpts) *Server {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
doc, ok := server.documents.Get(params.TextDocument.URI)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -228,7 +212,7 @@ func NewServer(opts ServerOpts) *Server {
|
||||
}
|
||||
|
||||
handler.TextDocumentHover = func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
doc, ok := server.documents.Get(params.TextDocument.URI)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -269,7 +253,7 @@ func NewServer(opts ServerOpts) *Server {
|
||||
}
|
||||
|
||||
handler.TextDocumentDocumentLink = func(context *glsp.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
doc, ok := server.documents.Get(params.TextDocument.URI)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -301,7 +285,7 @@ func NewServer(opts ServerOpts) *Server {
|
||||
}
|
||||
|
||||
handler.TextDocumentDefinition = func(context *glsp.Context, params *protocol.DefinitionParams) (interface{}, error) {
|
||||
doc, ok := server.documents[params.TextDocument.URI]
|
||||
doc, ok := server.documents.Get(params.TextDocument.URI)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@ -335,9 +319,201 @@ func NewServer(opts ServerOpts) *Server {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
path, 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
|
||||
}
|
||||
path = noteExists.Path
|
||||
}
|
||||
|
||||
// Index the notebook to be able to navigate to the new note.
|
||||
notebook.Index(false)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(filepath.Dir(doc.Path), path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
link, err := linkFormatter(relPath, opts.Title)
|
||||
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)
|
||||
}
|
||||
|
||||
if opts.Edit {
|
||||
go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
|
||||
URI: "file://" + path,
|
||||
TakeFocus: boolPtr(true),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return map[string]interface{}{"path": path}, nil
|
||||
}
|
||||
|
||||
func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
|
||||
return s.notebooks.Open(doc.Path)
|
||||
}
|
||||
@ -513,6 +689,10 @@ func rangeFromPosition(pos protocol.Position, startOffset, endOffset int) protoc
|
||||
}
|
||||
}
|
||||
|
||||
func isRangeEmpty(pos protocol.Range) bool {
|
||||
return pos.Start == pos.End
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool {
|
||||
b := v
|
||||
return &b
|
||||
@ -530,3 +710,16 @@ 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"
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/mickael-menu/zk/internal/util/errors"
|
||||
)
|
||||
|
||||
func pathToURI(path string) string {
|
||||
u := &url.URL{
|
||||
Scheme: "file",
|
||||
Path: path,
|
||||
Scheme: "file",
|
||||
Path: path,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
|
||||
func uriToPath(uri string) (string, error) {
|
||||
parsed, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
@ -25,3 +26,18 @@ func uriToPath(uri string) (string, error) {
|
||||
return parsed.Path, nil
|
||||
}
|
||||
|
||||
// jsonBoolean can be unmarshalled from integers or strings.
|
||||
// Neovim cannot send a boolean easily, so it's useful to support integers too.
|
||||
type jsonBoolean bool
|
||||
|
||||
func (b *jsonBoolean) UnmarshalJSON(data []byte) error {
|
||||
s := string(data)
|
||||
if s == "1" || s == "true" {
|
||||
*b = true
|
||||
} else if s == "0" || s == "false" {
|
||||
*b = false
|
||||
} else {
|
||||
return fmt.Errorf("%s: failed to unmarshal as boolean", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -2,16 +2,15 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/mickael-menu/zk/internal/core"
|
||||
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"
|
||||
"github.com/mickael-menu/zk/internal/util/strings"
|
||||
"github.com/tj/go-naturaldate"
|
||||
)
|
||||
|
||||
// Filtering holds filtering options to select notes.
|
||||
@ -205,14 +204,14 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
|
||||
opts.CreatedEnd = &end
|
||||
} else {
|
||||
if f.CreatedBefore != "" {
|
||||
date, err := parseDate(f.CreatedBefore)
|
||||
date, err := dateutil.TimeFromNatural(f.CreatedBefore)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
opts.CreatedEnd = &date
|
||||
}
|
||||
if f.CreatedAfter != "" {
|
||||
date, err := parseDate(f.CreatedAfter)
|
||||
date, err := dateutil.TimeFromNatural(f.CreatedAfter)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
@ -229,14 +228,14 @@ func (f Filtering) NewNoteFindOpts(notebook *core.Notebook) (core.NoteFindOpts,
|
||||
opts.ModifiedEnd = &end
|
||||
} else {
|
||||
if f.ModifiedBefore != "" {
|
||||
date, err := parseDate(f.ModifiedBefore)
|
||||
date, err := dateutil.TimeFromNatural(f.ModifiedBefore)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
opts.ModifiedEnd = &date
|
||||
}
|
||||
if f.ModifiedAfter != "" {
|
||||
date, err := parseDate(f.ModifiedAfter)
|
||||
date, err := dateutil.TimeFromNatural(f.ModifiedAfter)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
@ -266,15 +265,8 @@ func relPaths(notebook *core.Notebook, paths []string) ([]string, bool) {
|
||||
return relPaths, len(relPaths) > 0
|
||||
}
|
||||
|
||||
func parseDate(date string) (time.Time, error) {
|
||||
if i, err := strconv.ParseInt(date, 10, 0); err == nil && i >= 1000 && i < 5000 {
|
||||
return time.Date(int(i), time.January, 0, 0, 0, 0, 0, time.UTC), nil
|
||||
}
|
||||
return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))
|
||||
}
|
||||
|
||||
func parseDayRange(date string) (start time.Time, end time.Time, err error) {
|
||||
day, err := parseDate(date)
|
||||
day, err := dateutil.TimeFromNatural(date)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -49,15 +49,15 @@ type NoteIndex interface {
|
||||
// NoteIndexingStats holds statistics about a notebook indexing process.
|
||||
type NoteIndexingStats struct {
|
||||
// Number of notes in the source.
|
||||
SourceCount int
|
||||
SourceCount int `json:"sourceCount"`
|
||||
// Number of newly indexed notes.
|
||||
AddedCount int
|
||||
AddedCount int `json:"addedCount"`
|
||||
// Number of notes modified since last indexing.
|
||||
ModifiedCount int
|
||||
ModifiedCount int `json:"modifiedCount"`
|
||||
// Number of notes removed since last indexing.
|
||||
RemovedCount int
|
||||
RemovedCount int `json:"removedCount"`
|
||||
// Duration of the indexing process.
|
||||
Duration time.Duration
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
// String implements Stringer
|
||||
|
@ -1,6 +1,11 @@
|
||||
package date
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tj/go-naturaldate"
|
||||
)
|
||||
|
||||
// Provider returns a date instance.
|
||||
type Provider interface {
|
||||
@ -30,3 +35,14 @@ func NewFrozen(date time.Time) Frozen {
|
||||
func (n *Frozen) Date() time.Time {
|
||||
return n.date
|
||||
}
|
||||
|
||||
// TimeFromNatural parses a human date into a time.Time.
|
||||
func TimeFromNatural(date string) (time.Time, error) {
|
||||
if date == "" {
|
||||
return time.Now(), nil
|
||||
}
|
||||
if i, err := strconv.ParseInt(date, 10, 0); err == nil && i >= 1000 && i < 5000 {
|
||||
return time.Date(int(i), time.January, 0, 0, 0, 0, 0, time.UTC), nil
|
||||
}
|
||||
return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))
|
||||
}
|
||||
|
@ -29,3 +29,7 @@ func Wrap(err error, msg string) error {
|
||||
func New(text string) error {
|
||||
return errors.New(text)
|
||||
}
|
||||
|
||||
func As(err error, target interface{}) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user