Skip to content

Commit

Permalink
Add LSP custom commands and code actions to create new notes (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored May 11, 2021
1 parent 3664734 commit b17b42a
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 115 deletions.
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

0 comments on commit b17b42a

Please sign in to comment.