Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LSP custom commands and code actions to create new notes #40

Merged
merged 8 commits into from
May 11, 2021
Merged
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
99 changes: 94 additions & 5 deletions docs/editors-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -38,11 +43,39 @@ With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and ad
}
}
```
</details>

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
##### 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')
Expand All @@ -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": {
Expand All @@ -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.
66 changes: 64 additions & 2 deletions internal/adapter/lsp/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down
Loading