├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── LICENSE
├── README.md
├── autoload
├── lspoints.vim
└── lspoints
│ ├── denops.vim
│ ├── internal.vim
│ ├── settings.vim
│ └── util.vim
├── deno.json
├── denops
├── @ddc-sources
│ └── lspoints.ts
├── @lspoints
│ ├── format.ts
│ └── nvim_diagnostics.ts
└── lspoints
│ ├── app.ts
│ ├── box.ts
│ ├── builtin
│ ├── configuration.ts
│ ├── diagnostics.ts
│ └── did_save.ts
│ ├── builtins.ts
│ ├── client.ts
│ ├── deps
│ ├── async.ts
│ ├── denops.ts
│ ├── lsp.ts
│ ├── std
│ │ ├── async.ts
│ │ ├── bytes.ts
│ │ ├── deep_merge.ts
│ │ ├── path.ts
│ │ └── stream.ts
│ └── unknownutil.ts
│ ├── interface.ts
│ ├── jsonrpc
│ ├── jsonrpc_client.ts
│ ├── jsonrpc_stream.ts
│ └── message.ts
│ ├── lspoints.ts
│ └── version.ts
├── doc
├── config.ts
├── lspoints.jax
└── lspoints_example.vim
└── lua
└── lspoints.lua
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: github-actions
5 | directory: /
6 | schedule:
7 | interval: weekly
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | pull_request:
5 |
6 | jobs:
7 | deno-checks:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: denoland/setup-deno@v1
12 | with:
13 | deno-version: v1.x
14 | - name: Check types
15 | run: deno check denops
16 | - name: Lint
17 | run: deno lint denops
18 | - name: Check code formats
19 | run: deno fmt denops --check
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 kuuote and contributors
2 |
3 | This software is provided 'as-is', without any express or implied warranty.
4 | In no event will the authors be held liable for any damages arising from the use of this software.
5 |
6 | Permission is granted to anyone to use this software for any purpose,
7 | including commercial applications, and to alter it and redistribute it
8 | freely, subject to the following restrictions:
9 |
10 | 1. The origin of this software must not be misrepresented; you must not
11 | claim that you wrote the original software. If you use this software
12 | in a product, an acknowledgment in the product documentation would be
13 | appreciated but is not required.
14 |
15 | 2. Altered source versions must be plainly marked as such, and must not be
16 | misrepresented as being the original software.
17 |
18 | 3. This notice may not be removed or altered from any source distribution.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lspoints
2 |
3 | Asynchronous LSP Gateway Plugin powered by denops.vim
4 |
5 |
6 | > [!WARNING]
7 | >
8 | > As it is made for study, it may behave weird or break suddenly.
9 | >
10 |
11 | ## Installation
12 |
13 |
14 | You need to install lspoints with [denops](https://github.com/vim-denops/denops.vim) as follows:
15 |
16 | ```vim
17 | Plug 'vim-denops/denops.vim'
18 | Plug 'kuuote/lspoints'
19 | ```
20 |
21 | > [!IMPORTANT]
22 | >
23 | > As lspoints is a *gateway*, you achive no functionalities without extensions.
24 | > The following graph is the overview of the architecture.
25 | >
26 |
27 | ```mermaid
28 | graph LR
29 | subgraph Lspoints
30 | direction TB
31 | copilot-langauge-server ---|LSP| lspoints
32 | deno_lsp ---|LSP| lspoints
33 | efm-langserver ---|LSP| lspoints
34 | end
35 | lspoints --- lspoints-copilot
36 | lspoints --- ddc-source-lsp
37 | lspoints --- lspoints-hover
38 | lspoints --- nvim_diagnostics
39 | lspoints --- format
40 | format --- Vim/Neovim
41 | nvim_diagnostics --- Vim/Neovim
42 | lspoints-hover --- Vim/Neovim
43 | lspoints-copilot --- Vim/Neovim
44 | ddc-source-lsp --- Vim/Neovim
45 | ```
46 |
47 | ## Configuration
48 |
49 | To use lspoints, you need to configure two things.
50 |
51 | 1. What *language servers* are available.
52 | 2. What *extensions* are available.
53 |
54 |
55 | ### Language servers
56 |
57 | ```vim
58 | function s:attach_denols() abort
59 | call lspoints#attach('denols', #{
60 | \ cmd: ['deno', 'lsp'],
61 | \ initializationOptions: #{
62 | \ enable: v:true,
63 | \ unstable: v:true,
64 | \ suggest: #{
65 | \ autoImports: v:false,
66 | \ },
67 | \ },
68 | \ })
69 | endfunction
70 | autocmd FileType typescript,typescriptreact call s:attach_denols()
71 | ```
72 |
73 | ### Extensions
74 |
75 | You need to specify extension to be used.
76 | ```vim
77 | let g:lspoints#extensions = ['nvim_diagnostics', 'format']
78 | ```
79 |
80 | Alternatively, you can load extensions dynamically.
81 | ```vim
82 | call lspoints#load_extensions(['nvim_diagnostics', 'format'])
83 | ```
84 |
85 | ### Configuration example
86 |
87 |
88 |
89 | The example is just a combined version of the above code.
90 |
91 |
92 | ```vim
93 | let g:lspoints#extensions = ['nvim_diagnostics', 'format']
94 |
95 | function s:attach_denols() abort
96 | call lspoints#attach('denols', #{
97 | \ cmd: ['deno', 'lsp'],
98 | \ initializationOptions: #{
99 | \ enable: v:true,
100 | \ unstable: v:true,
101 | \ suggest: #{
102 | \ autoImports: v:false,
103 | \ },
104 | \ },
105 | \ })
106 | endfunction
107 | autocmd FileType typescript,typescriptreact call s:attach_denols()
108 | ```
109 |
110 |
111 |
112 |
113 | ## Further readings
114 |
115 | Again, you will not get any functionality without extensions. Please read the documentation of the extensions.
116 | You can find the lspoint extensions at https://github.com/search?q=lspoints&type=repositories .
117 |
118 |
119 | # nvim_diagnostics for lspoints (builtin)
120 |
121 | This is an extension to integrate lspoints into the diagnostic function of Neovim.
122 |
123 | ## Installation
124 |
125 | You need to add `'nvim_diagnostics'` to `g:lspoints#extensions`.
126 | ```vim
127 | let g:lspoints#extensions = ['nvim_diagnostics']
128 | ```
129 |
130 | ## Usage
131 |
132 | This extension automatically displays diagnostics in Neovim. You do not need to do anything.
133 |
134 | # format for lspoints (builtin)
135 |
136 | This is an extension to call format function of language servers.
137 |
138 | ## Installation
139 |
140 | You need to add `'format'` to `g:lspoints#extensions`.
141 | ```vim
142 | let g:lspoints#extensions = ['format']
143 | ```
144 |
145 | ## Usage
146 |
147 | When you call `denops#request('lspoints', 'executeCommand', ['format', 'execute', bufnr()])`, the language server formats your code.
148 |
149 | ```vim
150 | nnoremap mf call denops#request('lspoints', 'executeCommand', ['format', 'execute', bufnr()])
151 | ```
152 |
153 | ---
154 |
155 | © 2024 Kuuote
--------------------------------------------------------------------------------
/autoload/lspoints.vim:
--------------------------------------------------------------------------------
1 | let g:lspoints#extensions = get(g:, 'lspoints#extensions', [])
2 |
3 | " async method
4 |
5 | function lspoints#reload() abort
6 | autocmd User DenopsPluginPost:lspoints ++once echo 'lspoints reloaded'
7 | call denops#plugin#reload('lspoints')
8 | endfunction
9 |
10 | function lspoints#start(name, options = {}) abort
11 | call lspoints#denops#register()
12 | call lspoints#denops#notify('start', [a:name, a:options])
13 | endfunction
14 |
15 | function lspoints#attach(name, options = {}) abort
16 | call lspoints#start(a:name, a:options)
17 | let bufnr = bufnr()
18 | call lspoints#denops#notify('attach', [a:name, bufnr])
19 | endfunction
20 |
21 | function lspoints#load_extensions(pathes) abort
22 | call extend(g:lspoints#extensions, a:pathes)
23 | if denops#plugin#is_loaded('lspoints')
24 | call lspoints#denops#notify('loadExtensions', [a:pathes])
25 | endif
26 | endfunction
27 |
28 | function lspoints#notify(name, method, params = {}) abort
29 | call lspoints#denops#notify('notify', [a:name, a:method, a:params])
30 | endfunction
31 |
32 | function lspoints#request_async(name, method, params, success, failure) abort
33 | call lspoints#denops#request_async('request', [a:name, a:method, a:params], a:success, a:failure)
34 | endfunction
35 |
36 | " sync method
37 |
38 | function lspoints#get_clients(bufnr = bufnr()) abort
39 | return lspoints#denops#request('getClients', [a:bufnr])
40 | endfunction
41 |
42 | function lspoints#request(name, method, params = {}) abort
43 | return lspoints#denops#request('request', [a:name, a:method, a:params])
44 | endfunction
45 |
--------------------------------------------------------------------------------
/autoload/lspoints/denops.vim:
--------------------------------------------------------------------------------
1 | " lazy load logic based on ddu.vim
2 |
3 | let s:registered = v:false
4 |
5 | const s:root_dir = ''->expand()->fnamemodify(':h:h:h')
6 | const s:sep = has('win32') ? '\' : '/'
7 | function lspoints#denops#register() abort
8 | if s:registered
9 | return
10 | endif
11 | if denops#server#status() !=# 'running'
12 | autocmd User DenopsReady ++once call lspoints#denops#register()
13 | return
14 | endif
15 | let path = [s:root_dir, 'denops', 'lspoints', 'app.ts']->join(s:sep)
16 | try
17 | call denops#plugin#load('lspoints', path)
18 | catch /^Vim\%((\a\+)\)\=:E117:/
19 | " Fallback to `register` for backward compatibility
20 | call denops#plugin#register('lspoints', path, #{ mode: 'skip' })
21 | endtry
22 | let s:registered = v:true
23 | endfunction
24 |
25 | function lspoints#denops#notify(method, params) abort
26 | if !s:registered
27 | call lspoints#denops#register()
28 | endif
29 | call denops#plugin#wait_async('lspoints', {->denops#notify('lspoints', a:method, a:params)})
30 | endfunction
31 |
32 | function lspoints#denops#request(method, params, wait_async = v:false) abort
33 | if a:wait_async
34 | if !s:registered
35 | execute printf(
36 | \ 'autocmd User DenopsPluginPost:lspoints call denops#request("lspoints", "%s", %s)',
37 | \ a:method,
38 | \ string(a:params),
39 | \ )
40 | else
41 | call denops#plugin#wait_async('lspoints', {->denops#request('lspoints', a:method, a:params)})
42 | endif
43 | return
44 | endif
45 | if !s:registered
46 | throw 'lspoints is not registered'
47 | endif
48 | call denops#plugin#wait('lspoints')
49 | return denops#request('lspoints', a:method, a:params)
50 | endfunction
51 |
52 | function lspoints#denops#request_async(method, params, success, failure)
53 | if !s:registered
54 | call lspoints#denops#register()
55 | endif
56 | call denops#plugin#wait_async('lspoints', {->denops#request_async('lspoints', a:method, a:params, a:success, a:failure)})
57 | endfunction
58 |
--------------------------------------------------------------------------------
/autoload/lspoints/internal.vim:
--------------------------------------------------------------------------------
1 | function lspoints#internal#notify_change_params(bufnr) abort
2 | let changedtick = getbufvar(a:bufnr, 'changedtick')
3 | " Note: can't get changedtick from wiped buffer
4 | if empty(changedtick)
5 | return
6 | endif
7 | let uri = lspoints#util#bufnr_to_uri(a:bufnr)
8 | let text = lspoints#util#get_text(a:bufnr)
9 | return [a:bufnr, uri, text, changedtick]
10 | endfunction
11 |
12 | function lspoints#internal#notify_change(bufnr) abort
13 | let params = lspoints#internal#notify_change_params(a:bufnr)
14 | if empty(params)
15 | return
16 | endif
17 | call denops#notify('lspoints', 'notifyChange', params)
18 | endfunction
19 |
--------------------------------------------------------------------------------
/autoload/lspoints/settings.vim:
--------------------------------------------------------------------------------
1 | function! lspoints#settings#get() abort
2 | return lspoints#denops#request('getSettings', [])
3 | endfunction
4 |
5 | function! lspoints#settings#set(settings) abort
6 | call lspoints#denops#request('setSettings', [a:settings], v:true)
7 | endfunction
8 |
9 | function! lspoints#settings#patch(settings) abort
10 | call lspoints#denops#request('patchSettings', [a:settings], v:true)
11 | endfunction
12 |
--------------------------------------------------------------------------------
/autoload/lspoints/util.vim:
--------------------------------------------------------------------------------
1 | function lspoints#util#bufnr_to_uri(bufnr) abort
2 | let bufname = bufname(a:bufnr)->fnamemodify(':p')
3 | if bufname =~# ':/'
4 | return bufname
5 | else
6 | return 'file://' .. bufname
7 | endif
8 | endfunction
9 |
10 | " Vim script側でjoin等やるのが速いので問題出るまでこうする
11 | function lspoints#util#get_text(bufnr) abort
12 | let buf = getbufline(a:bufnr, 1, '$')
13 | " textDocumentの末尾には改行入ってるっぽいので
14 | eval buf->add('')
15 | return buf->join("\n")
16 | endfunction
17 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "exports": "./denops/lspoints/interface.ts",
3 | "lock": false,
4 | "name": "@kuuote/lspoints",
5 | "tasks": {
6 | "check": "deno fmt --check denops && deno lint && deno test -A",
7 | "fmt": "deno fmt denops"
8 | },
9 | "version": "0.1.1"
10 | }
11 |
--------------------------------------------------------------------------------
/denops/@ddc-sources/lspoints.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseSource,
3 | type GatherArguments,
4 | } from "https://deno.land/x/ddc_vim@v4.3.1/base/source.ts";
5 | import type { Item } from "https://deno.land/x/ddc_vim@v4.3.1/types.ts";
6 | import * as u from "https://deno.land/x/unknownutil@v3.16.3/mod.ts";
7 | import type * as LSPTypes from "npm:vscode-languageserver-types@3.17.5";
8 |
9 | type Never = Record;
10 |
11 | const isClients = u.isArrayOf(
12 | u.isObjectOf({
13 | name: u.isString,
14 | serverCapabilities: u.isRecord,
15 | }),
16 | );
17 |
18 | export class Source extends BaseSource {
19 | override async gather({
20 | denops,
21 | }: GatherArguments): Promise- {
22 | const bufNr = Number(await denops.call("bufnr"));
23 | const _clients = await denops.dispatch("lspoints", "getClients", bufNr);
24 | u.assert(_clients, isClients);
25 | const clients = _clients.filter((client) =>
26 | client.serverCapabilities.completionProvider != null
27 | );
28 | if (clients.length === 0) {
29 | return [];
30 | }
31 | const uri = String(
32 | await denops.call("lspoints#util#bufnr_to_uri", bufNr),
33 | );
34 | const line = Number(await denops.call("line", "."));
35 | const col = Number(await denops.call("col", "."));
36 | const params = {
37 | textDocument: {
38 | uri,
39 | },
40 | // TODO: マルチバイト文字を考慮していないのでちゃんと計算すること
41 | position: {
42 | line: line - 1,
43 | character: col - 1,
44 | },
45 | };
46 | const items = await Promise.all(clients.map(async (client) => {
47 | const result = await denops.dispatch(
48 | "lspoints",
49 | "request",
50 | client.name,
51 | "textDocument/completion",
52 | params,
53 | ) as {
54 | items: LSPTypes.CompletionItem[];
55 | } | null;
56 | if (result == null) {
57 | return [];
58 | }
59 | return Promise.resolve(result.items.map((i) => ({
60 | abbr: i.label,
61 | word: i.insertText ?? i.label,
62 | })));
63 | })).then((items) => items.flat());
64 | return items;
65 | }
66 |
67 | override params(): Never {
68 | return {};
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/denops/@lspoints/format.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "../lspoints/deps/denops.ts";
2 | import type { LSP } from "../lspoints/deps/lsp.ts";
3 | import { deadline } from "../lspoints/deps/std/async.ts";
4 | import { assert, is } from "../lspoints/deps/unknownutil.ts";
5 | import { BaseExtension, type Lspoints } from "../lspoints/interface.ts";
6 | import {
7 | applyTextEdits,
8 | uriFromBufnr,
9 | } from "jsr:@uga-rosa/denops-lsputil@^0.10.1";
10 |
11 | export class Extension extends BaseExtension {
12 | initialize(denops: Denops, lspoints: Lspoints) {
13 | lspoints.defineCommands("format", {
14 | execute: async (bufnr: unknown, timeout = 5000, selector?: unknown) => {
15 | assert(timeout, is.Number);
16 | assert(bufnr, is.Number);
17 | let clients = lspoints.getClients(bufnr).filter((c) =>
18 | c.serverCapabilities.documentFormattingProvider != null
19 | );
20 | if (is.String(selector)) {
21 | clients = clients.filter((c) => c.name === selector);
22 | }
23 |
24 | if (clients.length == 0) {
25 | throw Error("何のクライアントも選ばれてないわよ");
26 | }
27 |
28 | const resultPromise = lspoints.request(
29 | clients[0].name,
30 | "textDocument/formatting",
31 | {
32 | textDocument: {
33 | uri: await uriFromBufnr(denops, bufnr),
34 | },
35 | options: {
36 | tabSize: Number(await denops.call("shiftwidth")),
37 | insertSpaces: Boolean(await denops.eval("&l:expandtab")),
38 | },
39 | },
40 | ) as Promise;
41 | const result = await deadline(resultPromise, timeout)
42 | .catch(async () => {
43 | await denops.cmd(`
44 | echohl Error
45 | echomsg "Timeout!"
46 | echohl None
47 | `);
48 | });
49 | if (result == null) {
50 | return;
51 | }
52 | await applyTextEdits(denops, bufnr, result);
53 | },
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/denops/@lspoints/nvim_diagnostics.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "../lspoints/deps/denops.ts";
2 | import type { LSP } from "../lspoints/deps/lsp.ts";
3 | import { BaseExtension, type Lspoints } from "../lspoints/interface.ts";
4 |
5 | export class Extension extends BaseExtension {
6 | initialize(denops: Denops, lspoints: Lspoints) {
7 | if (denops.meta.host !== "nvim") {
8 | return;
9 | }
10 | lspoints.subscribeNotify(
11 | "textDocument/publishDiagnostics",
12 | async (client, _params) => {
13 | const params = _params as LSP.PublishDiagnosticsParams;
14 | const path = params.uri.replace(/^file:\/\//, "");
15 | const bufnr = Number(await denops.call("bufnr", path));
16 | if (bufnr == -1) {
17 | return;
18 | }
19 | await denops.call("luaeval", "require('lspoints').notify(_A)", {
20 | client,
21 | bufnr,
22 | diagnostics: params.diagnostics.map((d) => ({
23 | lnum: d.range.start.line,
24 | end_lnum: d.range["end"].line,
25 | col: d.range.start.character,
26 | end_col: d.range["end"].character,
27 | severity: d.severity,
28 | message: d.message,
29 | source: d.source,
30 | code: d.code,
31 | })),
32 | });
33 | },
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/denops/lspoints/app.ts:
--------------------------------------------------------------------------------
1 | import { loadBuiltins } from "./builtins.ts";
2 | import { autocmd, type Denops, type Entrypoint } from "./deps/denops.ts";
3 | import { as, assert, is } from "./deps/unknownutil.ts";
4 | import { isStartOptions, type Settings } from "./interface.ts";
5 | import { isArrayOrObject } from "./jsonrpc/message.ts";
6 | import { Lspoints } from "./lspoints.ts";
7 |
8 | const isNumberOrString = is.UnionOf([
9 | is.Number,
10 | is.String,
11 | ]);
12 |
13 | export const main: Entrypoint = async (denops: Denops) => {
14 | const lspoints = new Lspoints();
15 | denops.dispatcher = {
16 | getSettings() {
17 | return lspoints.settings.get();
18 | },
19 | setSettings(settings: unknown) {
20 | lspoints.settings.set(settings as Settings);
21 | },
22 | patchSettings(settings: unknown) {
23 | lspoints.settings.patch(settings as Settings);
24 | },
25 | async start(
26 | name: unknown,
27 | startOptions: unknown = {},
28 | ) {
29 | assert(name, is.String);
30 | assert(startOptions, isStartOptions);
31 | await lspoints.start(denops, name, startOptions);
32 | },
33 | async attach(id: unknown, bufNr: unknown) {
34 | assert(id, isNumberOrString);
35 | assert(bufNr, is.Number);
36 | await lspoints.attach(denops, id, bufNr);
37 | },
38 | async notifyChange(
39 | bufNr: unknown,
40 | uri: unknown,
41 | text: unknown,
42 | changedtick: unknown,
43 | ) {
44 | assert(bufNr, is.Number);
45 | assert(uri, is.String);
46 | assert(text, is.String);
47 | assert(changedtick, is.Number);
48 | await lspoints.notifyChange(bufNr, uri, text, changedtick);
49 | },
50 | getClients(bufNr: unknown) {
51 | assert(bufNr, is.Number);
52 | return lspoints.getClients(bufNr);
53 | },
54 | async notify(
55 | id: unknown,
56 | method: unknown,
57 | params: unknown = {},
58 | ): Promise {
59 | assert(id, isNumberOrString);
60 | assert(method, is.String);
61 | assert(params, as.Optional(isArrayOrObject));
62 | await lspoints.notify(id, method, params);
63 | },
64 | async request(
65 | id: unknown,
66 | method: unknown,
67 | params: unknown = {},
68 | ): Promise {
69 | assert(id, isNumberOrString);
70 | assert(method, is.String);
71 | assert(params, as.Optional(isArrayOrObject));
72 | return await lspoints.request(id, method, params);
73 | },
74 | async loadExtensions(ext: unknown) {
75 | assert(ext, is.ArrayOf(is.String));
76 | // from global, must be de-duplicate
77 | await lspoints.loadExtensions(denops, [...new Set(ext)]);
78 | },
79 | async executeCommand(
80 | extensionName: unknown,
81 | command: unknown,
82 | ...args: unknown[]
83 | ) {
84 | assert(extensionName, is.String);
85 | assert(command, is.String);
86 | return await lspoints.executeCommand(extensionName, command, ...args);
87 | },
88 | };
89 | await autocmd.group(denops, "lspoints.internal", (helper) => {
90 | helper.remove("*");
91 | helper.define("User", "LspointsAttach:*", ":");
92 | });
93 | await loadBuiltins(denops, lspoints);
94 | await denops.dispatcher.loadExtensions(
95 | await denops.eval("g:lspoints#extensions"),
96 | );
97 | return {
98 | [Symbol.asyncDispose]: async () => {
99 | lspoints.killall();
100 | await autocmd.group(denops, "lspoints.internal", (helper) => {
101 | helper.remove("*");
102 | });
103 | },
104 | };
105 | };
106 |
--------------------------------------------------------------------------------
/denops/lspoints/box.ts:
--------------------------------------------------------------------------------
1 | import { deepMerge, type DeepMergeOptions } from "./deps/std/deep_merge.ts";
2 |
3 | export class PatchableObjectBox> {
4 | #store: T;
5 | constructor(defaultValue: T) {
6 | this.#store = defaultValue;
7 | }
8 |
9 | get(): T {
10 | return this.#store;
11 | }
12 |
13 | set(value: T) {
14 | this.#store = value;
15 | }
16 |
17 | patch(value: Partial, options: DeepMergeOptions = {
18 | arrays: "replace",
19 | }) {
20 | this.#store = deepMerge(this.#store, value, options);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/denops/lspoints/builtin/configuration.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "../deps/denops.ts";
2 | import { as, assert, is, maybe } from "../deps/unknownutil.ts";
3 | import { BaseExtension, type Lspoints } from "../interface.ts";
4 |
5 | function getSettings(
6 | lspoints: Lspoints,
7 | clientName: string,
8 | ): Record | undefined {
9 | const client = lspoints.getClient(clientName);
10 | if (client == null) {
11 | return;
12 | }
13 | const settings = client.options.settings ?? client.options.params?.settings;
14 | return maybe(settings, is.Record);
15 | }
16 |
17 | export class Extension extends BaseExtension {
18 | initialize(_denops: Denops, lspoints: Lspoints) {
19 | lspoints.subscribeAttach(async (clientName, _bufnr) => {
20 | const settings = getSettings(lspoints, clientName);
21 | if (settings == null) {
22 | return;
23 | }
24 | await lspoints.notify(clientName, "workspace/didChangeConfiguration", {
25 | settings,
26 | });
27 | });
28 | lspoints.subscribeRequest(
29 | "workspace/configuration",
30 | (clientName, params) => {
31 | const settings = getSettings(lspoints, clientName);
32 | assert(
33 | params,
34 | is.ObjectOf({
35 | items: is.ArrayOf(is.ObjectOf({
36 | section: as.Optional(is.String),
37 | })),
38 | }),
39 | );
40 | return params.items.map((item) => settings?.[item.section!] ?? null);
41 | },
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/denops/lspoints/builtin/diagnostics.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "../deps/denops.ts";
2 | import type { LSP } from "../deps/lsp.ts";
3 | import { BaseExtension, type Lspoints } from "../interface.ts";
4 |
5 | type FlatDiagnostic = {
6 | client: string;
7 | bufnr: number;
8 | diagnostic: LSP.Diagnostic;
9 | };
10 |
11 | function ensure(map: Map, key: T, def: () => U): U {
12 | const value = map.get(key);
13 | if (value != null) {
14 | return value;
15 | }
16 | const defValue = def();
17 | map.set(key, defValue);
18 | return defValue;
19 | }
20 |
21 | export class Extension extends BaseExtension {
22 | diagnostics: Map>> = new Map();
23 |
24 | initialize(denops: Denops, lspoints: Lspoints) {
25 | lspoints.subscribeNotify(
26 | "textDocument/publishDiagnostics",
27 | async (clientName, _params) => {
28 | const params = _params as LSP.PublishDiagnosticsParams;
29 | const path = params.uri.replace(/^file:\/\//, "");
30 | const bufNr = Number(await denops.call("bufnr", path));
31 | if (bufNr == -1) {
32 | return;
33 | }
34 | const diagnosticsMap = ensure(
35 | this.diagnostics,
36 | clientName,
37 | () => new Map(),
38 | );
39 | if (params.diagnostics.length == 0) {
40 | diagnosticsMap.delete(bufNr);
41 | } else {
42 | diagnosticsMap.set(bufNr, params.diagnostics);
43 | }
44 | },
45 | );
46 | lspoints.defineCommands("lspoints.diagnostics", {
47 | get: () => this.diagnostics,
48 | getFlat: () => {
49 | const result: FlatDiagnostic[][] = [];
50 | for (const [client, nd] of this.diagnostics.entries()) {
51 | for (const [bufnr, diagnostics] of nd.entries()) {
52 | result.push(diagnostics.map((diagnostic) => ({
53 | client,
54 | bufnr,
55 | diagnostic,
56 | })));
57 | }
58 | }
59 | return result.flat();
60 | },
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/denops/lspoints/builtin/did_save.ts:
--------------------------------------------------------------------------------
1 | import { autocmd, type Denops } from "../deps/denops.ts";
2 | import { BaseExtension, type Lspoints } from "../interface.ts";
3 |
4 | export class Extension extends BaseExtension {
5 | async initialize(denops: Denops, lspoints: Lspoints) {
6 | await autocmd.group(denops, "lspoints.internal", (helper) => {
7 | helper.define(
8 | "BufWritePost",
9 | "*",
10 | "call denops#notify('lspoints', 'executeCommand', ['lspoints.did_save', 'handle', bufnr()])",
11 | );
12 | });
13 | lspoints.defineCommands("lspoints.did_save", {
14 | handle: (_bufnr: unknown) => {
15 | const bufnr = Number(_bufnr);
16 | for (const client of lspoints.getClients(bufnr)) {
17 | client.notify("textDocument/didSave", {
18 | textDocument: {
19 | uri: client.getUriFromBufNr(bufnr),
20 | },
21 | });
22 | }
23 | },
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/denops/lspoints/builtins.ts:
--------------------------------------------------------------------------------
1 | import { Extension as ConfigurationExtension } from "./builtin/configuration.ts";
2 | import { Extension as DiagnosticsExtension } from "./builtin/diagnostics.ts";
3 | import { Extension as DidSaveExtension } from "./builtin/did_save.ts";
4 | import type { Denops } from "./deps/denops.ts";
5 | import type { Lspoints } from "./interface.ts";
6 |
7 | export async function loadBuiltins(denops: Denops, lspoints: Lspoints) {
8 | new DiagnosticsExtension().initialize(denops, lspoints);
9 | new ConfigurationExtension().initialize(denops, lspoints);
10 | await new DidSaveExtension().initialize(denops, lspoints);
11 | }
12 |
--------------------------------------------------------------------------------
/denops/lspoints/client.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "./deps/denops.ts";
2 | import type { LSP } from "./deps/lsp.ts";
3 | import type { Settings, StartOptions } from "./interface.ts";
4 | import { JsonRpcClient, type LogHandler } from "./jsonrpc/jsonrpc_client.ts";
5 | import { version } from "./version.ts";
6 |
7 | export type ClientOptions = {
8 | rootPath?: string;
9 | rootUri?: string;
10 | initializationOptions?: Record;
11 | };
12 |
13 | async function createPrettyTracer(
14 | clientName: string,
15 | dir: string,
16 | ): Promise {
17 | await Deno.mkdir(dir).catch(() => {});
18 | const path = dir.replace(/\/?$/, "/") + `${clientName}_${Date.now()}.log`;
19 | async function write(type: string, msg: unknown) {
20 | const text = [
21 | `☆ ${type}`,
22 | JSON.stringify(msg, null, "\t"),
23 | "", // last newline
24 | ].join("\n");
25 | await Deno.writeTextFile(path, text, {
26 | append: true,
27 | });
28 | }
29 | return {
30 | onRead: async (msg) => {
31 | await write("r", msg);
32 | },
33 | onWrite: async (msg) => {
34 | await write("w", msg);
35 | },
36 | onStderr: async (msg) => {
37 | await write("e", msg);
38 | },
39 | };
40 | }
41 |
42 | let clientID = 0;
43 |
44 | export class LanguageClient {
45 | denops: Denops;
46 | name: string;
47 | options: StartOptions;
48 |
49 | id = clientID++;
50 | rpcClient: JsonRpcClient;
51 | #attachedBuffers: Record = {};
52 | #documentVersions: Record = {};
53 |
54 | serverCapabilities: LSP.ServerCapabilities = {};
55 |
56 | constructor(denops: Denops, name: string, startOptions: StartOptions) {
57 | this.denops = denops;
58 | this.name = name;
59 | this.options = startOptions;
60 |
61 | if (startOptions.cmd == null) {
62 | throw "cmd not specify";
63 | }
64 | this.rpcClient = new JsonRpcClient(
65 | startOptions.cmd,
66 | startOptions.cmdOptions,
67 | );
68 | }
69 |
70 | kill() {
71 | this.rpcClient.kill();
72 | }
73 |
74 | async initialize(settings: Settings) {
75 | if (settings.tracePath != null) {
76 | this.rpcClient.logger.subscribe(
77 | await createPrettyTracer(this.name, settings.tracePath),
78 | );
79 | }
80 | let rootUri: string | null = null;
81 | if (this.options.rootUri != null) {
82 | rootUri = String(this.options.rootUri);
83 | } else if (this.options.rootPath != null) {
84 | rootUri = "file://" + String(this.options.rootPath);
85 | }
86 | const response = await this.rpcClient.request(
87 | "initialize",
88 | {
89 | clientInfo: {
90 | name: "lspoints",
91 | version,
92 | },
93 | processId: Deno.pid,
94 | capabilities: settings.clientCapabilities,
95 | initializationOptions: this.options.initializationOptions ?? {},
96 | rootUri,
97 | } satisfies LSP.InitializeParams,
98 | ) as LSP.InitializeResult;
99 | await this.rpcClient.notify("initialized", {});
100 | this.serverCapabilities = response.capabilities;
101 | return this;
102 | }
103 |
104 | async attach(bufNr: number) {
105 | const params = await this.denops.call(
106 | "lspoints#internal#notify_change_params",
107 | bufNr,
108 | ) as [number, string, string, number] | 0;
109 | if (params !== 0) {
110 | await this.notifyChange(...params);
111 | }
112 | }
113 |
114 | getUriFromBufNr(bufNr: number) {
115 | return this.#attachedBuffers[bufNr] ?? "";
116 | }
117 |
118 | getDocumentVersion(bufNr: number) {
119 | return this.#documentVersions[this.#attachedBuffers[bufNr] ?? ""] ?? -1;
120 | }
121 |
122 | isAttached(bufNr: number): boolean {
123 | return this.#attachedBuffers[bufNr] != null;
124 | }
125 |
126 | async notifyChange(
127 | bufNr: number,
128 | uri: string,
129 | text: string,
130 | version: number,
131 | ) {
132 | const storedUri = this.#attachedBuffers[bufNr];
133 | // :saveasしたとかattachしてないとかでuri違ったらLS側に開き直すようにお願いする
134 | if (uri !== storedUri) {
135 | if (storedUri != null) {
136 | await this.rpcClient.notify("textDocument/didClose", {
137 | textDocument: {
138 | uri,
139 | },
140 | });
141 | }
142 | const filetype = String(
143 | await this.denops.call("getbufvar", bufNr, "&filetype"),
144 | );
145 | await this.rpcClient.notify("textDocument/didOpen", {
146 | textDocument: {
147 | uri,
148 | languageId: filetype,
149 | version,
150 | text,
151 | },
152 | });
153 | this.#attachedBuffers[bufNr] = uri;
154 | return;
155 | }
156 | this.#documentVersions[uri] = version;
157 | await this.rpcClient.notify("textDocument/didChange", {
158 | textDocument: {
159 | uri,
160 | version,
161 | },
162 | contentChanges: [{
163 | text,
164 | }],
165 | });
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/async.ts:
--------------------------------------------------------------------------------
1 | export { Lock } from "jsr:@lambdalisue/async@^2.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/denops.ts:
--------------------------------------------------------------------------------
1 | export * as autocmd from "jsr:@denops/std@^7.0.0/autocmd";
2 | export type { Denops, Entrypoint } from "jsr:@denops/std@^7.0.0";
3 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/lsp.ts:
--------------------------------------------------------------------------------
1 | export * as LSP from "npm:vscode-languageserver-protocol@3.17.5";
2 | export * as LSPTypes from "npm:vscode-languageserver-types@3.17.5";
3 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/std/async.ts:
--------------------------------------------------------------------------------
1 | export { deadline } from "jsr:@std/async@^1.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/std/bytes.ts:
--------------------------------------------------------------------------------
1 | export { concat, indexOfNeedle } from "jsr:@std/bytes@^1.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/std/deep_merge.ts:
--------------------------------------------------------------------------------
1 | export { deepMerge, type DeepMergeOptions } from "jsr:@std/collections@^1.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/std/path.ts:
--------------------------------------------------------------------------------
1 | export * as stdpath from "jsr:@std/path@^1.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/std/stream.ts:
--------------------------------------------------------------------------------
1 | export { TextLineStream } from "jsr:@std/streams@^1.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/deps/unknownutil.ts:
--------------------------------------------------------------------------------
1 | export * from "jsr:@core/unknownutil@^4.0.0";
2 |
--------------------------------------------------------------------------------
/denops/lspoints/interface.ts:
--------------------------------------------------------------------------------
1 | import type { PatchableObjectBox } from "./box.ts";
2 | import type { Denops } from "./deps/denops.ts";
3 | import type { LSP } from "./deps/lsp.ts";
4 | import { as, is, type Predicate } from "./deps/unknownutil.ts";
5 |
6 | type Promisify = T | Promise;
7 |
8 | // deno-lint-ignore no-explicit-any
9 | type ArrayOrObject = Array | Record;
10 |
11 | export type StartOptions = {
12 | cmd?: string[];
13 | cmdOptions?: Record;
14 | initializationOptions?: Record;
15 | settings?: Record;
16 | params?: Record;
17 | rootPath?: string;
18 | rootUri?: string;
19 | };
20 |
21 | export const isStartOptions: Predicate = is.ObjectOf({
22 | cmd: as.Optional(is.ArrayOf(is.String)),
23 | cmdOptions: as.Optional(is.RecordOf(is.Unknown)),
24 | initializationOptions: as.Optional(is.Record),
25 | settings: as.Optional(is.Record),
26 | params: as.Optional(is.Record),
27 | rootPath: as.Optional(is.String),
28 | rootUri: as.Optional(is.String),
29 | });
30 |
31 | export type Client = {
32 | name: string;
33 | id: number;
34 | notify: (method: string, params?: ArrayOrObject) => Promise;
35 | request: (
36 | method: string,
37 | params?: ArrayOrObject,
38 | options?: { signal: AbortSignal },
39 | ) => Promise;
40 | serverCapabilities: LSP.ServerCapabilities;
41 | getUriFromBufNr(bufnr: number): string;
42 | getDocumentVersion(bufnr: number): number;
43 | isAttached(bufnr: number): boolean;
44 | options: StartOptions;
45 | };
46 |
47 | export type Settings = {
48 | clientCapabilities: LSP.ClientCapabilities;
49 | startOptions: Record;
50 | requestTimeout: number;
51 | tracePath?: string;
52 | };
53 |
54 | export type AttachCallback = (
55 | clientName: string,
56 | bufnr: number,
57 | ) => Promisify;
58 |
59 | export type NotifyCallback = (
60 | clientName: string,
61 | params: unknown,
62 | ) => Promisify;
63 |
64 | export type RequestCallback = (
65 | clientName: string,
66 | params: unknown,
67 | ) => Promisify;
68 |
69 | export type CommandResult = unknown | Promise;
70 | export type Command = (...args: unknown[]) => CommandResult;
71 |
72 | export interface Lspoints {
73 | readonly settings: PatchableObjectBox;
74 |
75 | getClient(name: string): Client | undefined;
76 |
77 | getClients(bufNr: number): Client[];
78 |
79 | notify(
80 | name: string,
81 | method: string,
82 | params: ArrayOrObject,
83 | ): Promise;
84 |
85 | request(
86 | name: string,
87 | method: string,
88 | params: ArrayOrObject,
89 | ): Promise;
90 |
91 | subscribeAttach(callback: AttachCallback): void;
92 |
93 | subscribeNotify(
94 | method: string,
95 | callback: NotifyCallback,
96 | ): void;
97 |
98 | subscribeRequest(
99 | method: string,
100 | callback: RequestCallback,
101 | ): void;
102 |
103 | defineCommands(
104 | extensionName: string,
105 | commands: Record,
106 | ): void;
107 |
108 | executeCommand(
109 | extensionName: string,
110 | command: string,
111 | ...args: unknown[]
112 | ): CommandResult;
113 | }
114 |
115 | export abstract class BaseExtension {
116 | abstract initialize(denops: Denops, lspoints: Lspoints): void | Promise;
117 | }
118 |
--------------------------------------------------------------------------------
/denops/lspoints/jsonrpc/jsonrpc_client.ts:
--------------------------------------------------------------------------------
1 | import { concat } from "../deps/std/bytes.ts";
2 | import { TextLineStream } from "../deps/std/stream.ts";
3 | import { JsonRpcStream } from "./jsonrpc_stream.ts";
4 | import {
5 | isNotifyMessage,
6 | isRequestMessage,
7 | isResponseMessage,
8 | type NotifyMessage,
9 | type RequestMessage,
10 | } from "./message.ts";
11 |
12 | type Promisify = T | Promise;
13 |
14 | const encoder = new TextEncoder();
15 |
16 | function jsonRpcEncode(data: unknown): Uint8Array {
17 | const buf = encoder.encode(JSON.stringify(data));
18 | const header = `Content-Length: ${buf.byteLength}\r\n\r\n`;
19 | const headerRaw = encoder.encode(header);
20 | return concat([headerRaw, buf]);
21 | }
22 |
23 | class Logger {
24 | handlers = new Array();
25 |
26 | onRead(msg: unknown) {
27 | for (const handler of this.handlers) {
28 | handler.onRead?.(msg);
29 | }
30 | }
31 | onWrite(msg: unknown) {
32 | for (const handler of this.handlers) {
33 | handler.onWrite?.(msg);
34 | }
35 | }
36 | onStderr(msg: string) {
37 | for (const handler of this.handlers) {
38 | handler.onStderr?.(msg);
39 | }
40 | }
41 | subscribe(handler: LogHandler) {
42 | this.handlers.push(handler);
43 | }
44 | }
45 |
46 | export interface LogHandler {
47 | onRead?: (msg: unknown) => Promisify;
48 | onWrite?: (msg: unknown) => Promisify;
49 | onStderr?: (msg: string) => Promisify;
50 | }
51 |
52 | export class JsonRpcClient {
53 | #process: Deno.ChildProcess;
54 | #w: WritableStreamDefaultWriter;
55 |
56 | #requestId = 0;
57 | #requestPool: Record void, (e: unknown) => void]> =
58 | {};
59 |
60 | notifyHandlers: Array<(msg: NotifyMessage) => Promisify> = [];
61 | requestHandlers: Array<
62 | (msg: RequestMessage) => Promisify
63 | > = [];
64 | logger = new Logger();
65 |
66 | constructor(
67 | command: string[],
68 | options?: Record,
69 | ) {
70 | this.#process = new Deno.Command(command[0], {
71 | ...options,
72 | args: command.slice(1),
73 | stdin: "piped",
74 | stdout: "piped",
75 | stderr: "piped",
76 | }).spawn();
77 | this.#process.stdout
78 | .pipeThrough(new JsonRpcStream())
79 | .pipeTo(
80 | new WritableStream({
81 | write: (chunk: unknown) => {
82 | this.logger.onRead(chunk);
83 | if (isRequestMessage(chunk)) {
84 | (async () => {
85 | for (const handler of this.requestHandlers) {
86 | const result = await handler(chunk);
87 | if (result !== undefined) {
88 | this.#sendMessage({
89 | jsonrpc: "2.0",
90 | id: chunk.id,
91 | result,
92 | });
93 | return;
94 | }
95 | }
96 | })().catch(console.trace);
97 | } else if (isResponseMessage(chunk)) {
98 | const id = Number(chunk.id);
99 | const cb = this.#requestPool[id];
100 | if (cb == null) {
101 | // console.log("unresolved response: " + id);
102 | // console.log(chunk);
103 | } else {
104 | if (chunk.result !== undefined) { // contains null
105 | cb[0](chunk.result);
106 | } else if (chunk.error != null) {
107 | cb[1](JSON.stringify(chunk.error));
108 | }
109 | delete this.#requestPool[id];
110 | }
111 | } else if (isNotifyMessage(chunk)) {
112 | for (const notifier of this.notifyHandlers) {
113 | notifier(chunk)?.catch(console.log);
114 | }
115 | } else {
116 | console.log("unresolved chunk");
117 | console.log(chunk);
118 | }
119 | },
120 | }),
121 | );
122 | this.#process.stderr
123 | .pipeThrough(new TextDecoderStream())
124 | .pipeThrough(new TextLineStream())
125 | .pipeTo(
126 | new WritableStream({
127 | write: (line: string) => {
128 | this.logger.onStderr(line);
129 | },
130 | }),
131 | );
132 | this.#w = this.#process.stdin.getWriter();
133 | }
134 |
135 | kill() {
136 | this.#process.kill();
137 | }
138 |
139 | async #sendMessage(msg: unknown) {
140 | this.logger.onWrite(msg);
141 | await this.#w.write(jsonRpcEncode(msg));
142 | }
143 |
144 | async notify(
145 | method: string,
146 | params?: unknown[] | Record,
147 | ): Promise {
148 | const msg: NotifyMessage = {
149 | jsonrpc: "2.0",
150 | method,
151 | };
152 | if (params != null) {
153 | msg.params = params;
154 | }
155 | await this.#sendMessage(msg);
156 | }
157 |
158 | async request(
159 | method: string,
160 | params?: unknown[] | Record,
161 | options?: { signal?: AbortSignal },
162 | ): Promise {
163 | const id = this.#requestId++;
164 | const msg: RequestMessage = {
165 | jsonrpc: "2.0",
166 | id,
167 | method,
168 | };
169 | if (params != null) {
170 | msg.params = params;
171 | }
172 | await this.#sendMessage(msg);
173 | options?.signal?.addEventListener("abort", () => {
174 | this.notify("$/cancelRequest", { id });
175 | });
176 | return new Promise((resolve, reject) => {
177 | this.#requestPool[id] = [resolve, reject];
178 | });
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/denops/lspoints/jsonrpc/jsonrpc_stream.ts:
--------------------------------------------------------------------------------
1 | import { concat, indexOfNeedle } from "../deps/std/bytes.ts";
2 |
3 | const rn = new Uint8Array([0xd, 0xa]);
4 | const decoder = new TextDecoder();
5 |
6 | enum Mode {
7 | Header = 0,
8 | Content = 1,
9 | }
10 |
11 | export class JsonRpcStream extends TransformStream {
12 | #mode = Mode.Header;
13 | #contentLength = -1;
14 | #buf = new Uint8Array();
15 |
16 | constructor() {
17 | super({
18 | transform: (chunk, controller) => {
19 | this.#handle(chunk, controller);
20 | },
21 | });
22 | }
23 |
24 | #handle(
25 | chunk: Uint8Array,
26 | controller: TransformStreamDefaultController,
27 | ) {
28 | this.#buf = concat([this.#buf, chunk]);
29 | while (true) {
30 | if (this.#mode === Mode.Header) {
31 | while (true) {
32 | const found = indexOfNeedle(this.#buf, rn);
33 | if (found === -1) {
34 | return;
35 | }
36 | const headerRaw = this.#buf.subarray(0, found);
37 | this.#buf = this.#buf.subarray(found + 2);
38 | if (headerRaw.length === 0) {
39 | // terminal
40 | if (this.#contentLength === -1) {
41 | throw new Error("Content-Lengthが指定されてねーぞゴルァ");
42 | }
43 | this.#mode = Mode.Content;
44 | break;
45 | } else {
46 | const header = decoder.decode(headerRaw);
47 | const match = header.match(/Content-Length: (\d+)/);
48 | const length = Number(match?.[1]);
49 | if (!isNaN(length)) {
50 | this.#contentLength = length;
51 | }
52 | }
53 | }
54 | }
55 | if (this.#mode === Mode.Content) {
56 | if (this.#contentLength <= this.#buf.length) {
57 | const content = this.#buf.subarray(0, this.#contentLength);
58 | this.#buf = this.#buf.subarray(this.#contentLength);
59 | controller.enqueue(JSON.parse(decoder.decode(content)));
60 | this.#mode = Mode.Header;
61 | } else {
62 | return;
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/denops/lspoints/jsonrpc/message.ts:
--------------------------------------------------------------------------------
1 | import { as, is, type PredicateType } from "../deps/unknownutil.ts";
2 |
3 | export const isArrayOrObject = is.UnionOf([
4 | is.Array,
5 | is.RecordOf(is.Any),
6 | ]);
7 |
8 | export type ArrayOrObject = PredicateType;
9 |
10 | const isMessage = is.ObjectOf({
11 | jsonrpc: is.LiteralOf("2.0"),
12 | });
13 |
14 | export const isNotifyMessage = is.IntersectionOf([
15 | isMessage,
16 | is.ObjectOf({
17 | method: is.String,
18 | params: as.Optional(isArrayOrObject),
19 | }),
20 | ]);
21 |
22 | export type NotifyMessage = PredicateType;
23 |
24 | const isID = is.UnionOf([
25 | is.Number,
26 | is.String,
27 | ]);
28 |
29 | export const isRequestMessage = is.IntersectionOf([
30 | isNotifyMessage,
31 | is.ObjectOf({
32 | id: isID,
33 | }),
34 | ]);
35 |
36 | export type RequestMessage = PredicateType;
37 |
38 | const isResponseError = is.ObjectOf({
39 | code: is.Number,
40 | message: is.String,
41 | data: as.Optional(is.UnionOf([
42 | is.String,
43 | is.Number,
44 | is.Boolean,
45 | isArrayOrObject,
46 | is.Null,
47 | ])),
48 | });
49 |
50 | export const isResponseMessage = is.IntersectionOf([
51 | isMessage,
52 | is.ObjectOf({
53 | id: isID,
54 | result: as.Optional(is.UnionOf([
55 | is.String,
56 | is.Number,
57 | is.Boolean,
58 | isArrayOrObject,
59 | is.Null,
60 | ])),
61 | error: as.Optional(isResponseError),
62 | }),
63 | ]);
64 |
65 | export type ResponseMessage = PredicateType;
66 |
--------------------------------------------------------------------------------
/denops/lspoints/lspoints.ts:
--------------------------------------------------------------------------------
1 | import { PatchableObjectBox } from "./box.ts";
2 | import { LanguageClient } from "./client.ts";
3 | import { Lock } from "./deps/async.ts";
4 | import { autocmd, type Denops } from "./deps/denops.ts";
5 | import { deadline } from "./deps/std/async.ts";
6 | import { deepMerge } from "./deps/std/deep_merge.ts";
7 | import { stdpath } from "./deps/std/path.ts";
8 | import type {
9 | AttachCallback,
10 | BaseExtension,
11 | Client,
12 | Command,
13 | NotifyCallback,
14 | RequestCallback,
15 | Settings,
16 | StartOptions,
17 | } from "./interface.ts";
18 | import type { ArrayOrObject } from "./jsonrpc/message.ts";
19 |
20 | const lock = new Lock(null);
21 |
22 | const clientCache = new WeakMap();
23 | // transform client implementation to description object
24 | function transformClient(client: LanguageClient): Client {
25 | const cached = clientCache.get(client);
26 | if (cached != null) {
27 | return cached;
28 | }
29 | const transformed: Client = {
30 | name: client.name,
31 | id: client.id,
32 | notify: client.rpcClient.notify.bind(client.rpcClient),
33 | request: client.rpcClient.request.bind(client.rpcClient),
34 | serverCapabilities: client.serverCapabilities,
35 | getUriFromBufNr: client.getUriFromBufNr.bind(client),
36 | getDocumentVersion: client.getDocumentVersion.bind(client),
37 | isAttached: client.isAttached.bind(client),
38 | options: client.options,
39 | };
40 | clientCache.set(client, transformed);
41 | return transformed;
42 | }
43 |
44 | export class Lspoints {
45 | commands: Record> = {};
46 | attachHandlers: Array = [];
47 | notifyHandlers: Record> = {};
48 | requestHandlers: Record> = {};
49 | clients: Record = {};
50 | clientIDs: Record = {};
51 | settings: PatchableObjectBox = new PatchableObjectBox({
52 | clientCapabilities: {
53 | general: {
54 | positionEncodings: [
55 | "utf-16",
56 | ],
57 | },
58 | textDocument: {
59 | // https://github.com/hrsh7th/cmp-nvim-lsp/blob/44b16d11215dce86f253ce0c30949813c0a90765/lua/cmp_nvim_lsp/init.lua#L37
60 | completion: {
61 | dynamicRegistration: false,
62 | completionItem: {
63 | snippetSupport: true,
64 | commitCharactersSupport: true,
65 | deprecatedSupport: true,
66 | preselectSupport: true,
67 | tagSupport: {
68 | valueSet: [
69 | 1, // Deprecated
70 | ],
71 | },
72 | insertReplaceSupport: true,
73 | resolveSupport: {
74 | properties: [
75 | "documentation",
76 | "detail",
77 | "additionalTextEdits",
78 | "sortText",
79 | "filterText",
80 | "insertText",
81 | "textEdit",
82 | "insertTextFormat",
83 | "insertTextMode",
84 | ],
85 | },
86 | insertTextModeSupport: {
87 | valueSet: [
88 | 1, // asIs
89 | 2, // adjustIndentation
90 | ],
91 | },
92 | labelDetailsSupport: true,
93 | },
94 | contextSupport: true,
95 | insertTextMode: 1,
96 | completionList: {
97 | itemDefaults: [
98 | "commitCharacters",
99 | "editRange",
100 | "insertTextFormat",
101 | "insertTextMode",
102 | "data",
103 | ],
104 | },
105 | },
106 | },
107 | },
108 | requestTimeout: 5000,
109 | startOptions: {},
110 | });
111 |
112 | #getClient(id: number | string): LanguageClient | undefined {
113 | if (typeof id === "number") {
114 | return this.clientIDs[id];
115 | } else {
116 | return this.clients[id];
117 | }
118 | }
119 |
120 | async start(
121 | denops: Denops,
122 | name: string,
123 | options: StartOptions = {},
124 | ) {
125 | if (this.clients[name] == null) {
126 | await lock.lock(async () => {
127 | const defaultOptions = this.settings.get().startOptions[name];
128 | if (defaultOptions != null) {
129 | options = deepMerge(defaultOptions, options, {
130 | arrays: "replace",
131 | });
132 | }
133 | // TODO: TCP接続とか対応する
134 | const client = await new LanguageClient(
135 | denops,
136 | name,
137 | options,
138 | )
139 | .initialize(this.settings.get());
140 | this.clients[name] = client;
141 | this.clientIDs[client.id] = client;
142 | client.rpcClient.notifyHandlers.push(async (msg) => {
143 | for (const notifier of this.notifyHandlers[msg.method] ?? []) {
144 | await notifier(name, msg.params);
145 | }
146 | });
147 | client.rpcClient.requestHandlers.push(async (msg) => {
148 | for (const handler of this.requestHandlers[msg.method] ?? []) {
149 | const result = await handler(name, msg.params);
150 | if (result !== undefined) {
151 | return result;
152 | }
153 | }
154 | });
155 | });
156 | }
157 | }
158 |
159 | // kill all clients to avoids dangling process
160 | killall() {
161 | for (const client of Object.values(this.clients)) {
162 | client.kill();
163 | }
164 | }
165 |
166 | async attach(denops: Denops, id: string | number, bufNr: number) {
167 | let name = "";
168 | await lock.lock(async () => {
169 | const client = this.#getClient(id);
170 | if (client == null) {
171 | throw Error(`client "${id}" is not started`);
172 | }
173 | name = client.name;
174 | await client.attach(bufNr);
175 | });
176 | for (const handler of this.attachHandlers) {
177 | await handler(name, bufNr);
178 | }
179 | await autocmd.group(
180 | denops,
181 | "lspoints.internal",
182 | (helper) => {
183 | helper.remove("*", "");
184 | helper.define(
185 | ["TextChanged", "TextChangedI"],
186 | "",
187 | `call lspoints#internal#notify_change(${bufNr})`,
188 | );
189 | },
190 | );
191 | await autocmd.emit(denops, "User", `LspointsAttach:${name}:${bufNr}`);
192 | }
193 |
194 | async notifyChange(
195 | bufNr: number,
196 | uri: string,
197 | text: string,
198 | changedtick: number,
199 | ) {
200 | const clients = Object.values(this.clients)
201 | .filter((client) => client.isAttached(bufNr));
202 | for (const client of clients) {
203 | await client.notifyChange(bufNr, uri, text, changedtick);
204 | }
205 | }
206 |
207 | getClient(name: number | string): Client | undefined {
208 | const client = this.#getClient(name);
209 | if (client == null) {
210 | return;
211 | }
212 | return transformClient(client);
213 | }
214 |
215 | getClients(bufNr: number): Client[] {
216 | return Object.entries(this.clients)
217 | .sort((a, b) => a[0].localeCompare(b[0]))
218 | .filter((entry) => entry[1].isAttached(bufNr))
219 | .map((entry) => transformClient(entry[1]));
220 | }
221 |
222 | async notify(
223 | id: string | number,
224 | method: string,
225 | params: ArrayOrObject = {},
226 | ): Promise {
227 | if (lock.locked) {
228 | await lock.lock(() => {});
229 | }
230 | const client = this.#getClient(id);
231 | if (client == null) {
232 | throw Error(`client "${id}" is not started`);
233 | }
234 | await client.rpcClient.notify(method, params);
235 | }
236 |
237 | async request(
238 | id: string | number,
239 | method: string,
240 | params: ArrayOrObject = {},
241 | ): Promise {
242 | if (lock.locked) {
243 | await lock.lock(() => {});
244 | }
245 | const client = this.#getClient(id);
246 | if (client == null) {
247 | throw Error(`client "${id}" is not started`);
248 | }
249 | const timeout = this.settings.get().requestTimeout;
250 | return await deadline(client.rpcClient.request(method, params), timeout)
251 | .catch((e) => {
252 | if (!(e instanceof DOMException)) {
253 | return Promise.reject(e);
254 | }
255 | return client.denops.cmd(
256 | [
257 | "echohl Error",
258 | 'echomsg "lspoints: request timeout"',
259 | "echomsg client",
260 | "echomsg method",
261 | "for p in params | echomsg p | endfor",
262 | "echohl None",
263 | ].join("\n"),
264 | {
265 | client: "client: " + client.name,
266 | method: "method: " + method,
267 | params: ("params: " + JSON.stringify(params, undefined, " "))
268 | .split("\n"),
269 | },
270 | );
271 | });
272 | }
273 |
274 | async loadExtensions(denops: Denops, path: string[]) {
275 | for (let p of path) {
276 | if (p.indexOf("/") == -1) {
277 | p = String(
278 | await denops.eval(
279 | `globpath(&runtimepath, 'denops/@lspoints/${p}.ts')`,
280 | ),
281 | ).replace(/\n.*/, "");
282 | }
283 | // NOTE: Import module with fragment so that reload works properly.
284 | // https://github.com/vim-denops/denops.vim/issues/227
285 | const mod = await import(
286 | `${stdpath.toFileUrl(p).href}#${performance.now()}`
287 | );
288 | await (new mod.Extension() as BaseExtension).initialize(denops, this);
289 | }
290 | }
291 |
292 | subscribeAttach(callback: AttachCallback) {
293 | (this.attachHandlers = this.attachHandlers ?? []).push(callback);
294 | }
295 |
296 | subscribeNotify(
297 | method: string,
298 | callback: NotifyCallback,
299 | ) {
300 | (this.notifyHandlers[method] = this.notifyHandlers[method] ?? []).push(
301 | callback,
302 | );
303 | }
304 |
305 | subscribeRequest(
306 | method: string,
307 | callback: RequestCallback,
308 | ) {
309 | (this.requestHandlers[method] = this.requestHandlers[method] ?? []).push(
310 | callback,
311 | );
312 | }
313 |
314 | defineCommands(extensionName: string, commands: Record) {
315 | this.commands[extensionName] = commands;
316 | }
317 |
318 | async executeCommand(
319 | extensionName: string,
320 | command: string,
321 | ...args: unknown[]
322 | ): Promise {
323 | return await this.commands[extensionName][command](...args);
324 | }
325 | }
326 |
--------------------------------------------------------------------------------
/denops/lspoints/version.ts:
--------------------------------------------------------------------------------
1 | export const version = "v0.1.1";
2 |
--------------------------------------------------------------------------------
/doc/config.ts:
--------------------------------------------------------------------------------
1 | import type { Denops } from "jsr:@denops/std@7.0.0";
2 | import {
3 | BaseExtension,
4 | type Lspoints,
5 | } from "https://deno.land/x/lspoints@v0.0.7/interface.ts";
6 |
7 | // place to {runtimepath}/denops/@lspoints/config.ts
8 |
9 | export class Extension extends BaseExtension {
10 | override initialize(_denops: Denops, lspoints: Lspoints) {
11 | lspoints.settings.patch({
12 | startOptions: {
13 | // TypeScript way to given options
14 | denols: {
15 | cmd: ["deno", "lsp"],
16 | settings: {
17 | deno: {
18 | enable: true,
19 | unstable: true,
20 | },
21 | },
22 | },
23 | luals: {
24 | cmd: ["lua-language-server"],
25 | settings: {
26 | Lua: {
27 | diagnostics: {
28 | globals: ["vim"],
29 | },
30 | },
31 | },
32 | },
33 | },
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/doc/lspoints.jax:
--------------------------------------------------------------------------------
1 | *lspoints.txt* denops.vimを使ってLSPをお喋りします。
2 |
3 | help書けてないのでぼちぼち書きます
4 | 暫定的に使ってる設定の写しが `./lspoints_example.vim` にあります(|gf|で見れます)
5 |
6 | ==============================================================================
7 | ☆概要 *lspoints-introduction*
8 |
9 | *lspoints* は Language Server と Vim のバッファを接続し、リクエストを送信した
10 | り、診断などの通知を受信したりするための機能を提供します。
11 |
12 | *lspoints-synopsis*
13 | >
14 | " カレントバッファに `deno lsp` を attach する
15 | function s:attach_denols() abort
16 | call lspoints#attach('denols', #{
17 | \ cmd: ['deno', 'lsp'],
18 | \ initializationOptions: #{
19 | \ enable: v:true,
20 | \ unstable: v:true,
21 | \ suggest: #{
22 | \ autoImports: v:false,
23 | \ },
24 | \ },
25 | \ })
26 | endfunction
27 | call s:attach_denols()
28 |
29 | " ファイルを開いた時に attach する
30 | autocmd FileType typescript,typescriptreact call s:attach_denols()
31 |
32 | " 起動時に拡張を読み込む
33 | autocmd User DenopsPluginPost:lspoints
34 | \ call lspoints#load_extensions([
35 | \ 'nvim_diagnostics',
36 | \ ])
37 |
38 | <
39 | ==============================================================================
40 | API *lspoints-api*
41 |
42 | *lspoints#reload()*
43 | lspoints#reload()
44 | denops.vimに命令を送り、lspointsの再起動を行います。
45 | サーバーは全て終了し、アタッチも全て解除されます。
46 | 自動でサーバーが起動したりはしないため、何らかの方法で再起動する必要
47 | があります。
48 |
49 | *lspoints#start()*
50 | lspoints#start({name}, [{options}])
51 | {name} で示される言語サーバーを起動します。
52 | は言語サーバーが立ち上がっていない時に起動するために使用されます。
53 | {options} は以下の要素から成ります:
54 |
55 | cmd:
56 | サーバーを起動するためのコマンドライン。文字列のリスト。
57 | cmdOptions:
58 | サーバーを起動する際に `Deno.Command()` の `options` 引数に渡
59 | す辞書です。ただし `args`, `stdin`, `stdout`, `sterr`の4つの
60 | フィールドの値は内部的に決定されます。
61 | initializationOptions:
62 | サーバーに渡される初期化オプション
63 | settings:
64 | ワークスペース設定の初期値。
65 | `workspace/didChangeConfiguration` 及び
66 | `workspace/configuration` によりサーバーに渡されます。
67 |
68 | *lspoints#attach()*
69 | lspoints#attach({name}, [{options}])
70 | カレントバッファに {name} で示される言語サーバーをアタッチします。
71 | {options} は言語サーバーが立ち上がっていない時に起動するために
72 | 使用されます。立ち上がっている場合は無視されるため、省略も可能です。
73 | |lspoints#start()| に渡されます。
74 |
75 | *lspoints#load_extensions()*
76 | lspoints#load_extensions({pathes})
77 | {pathes} に指定されたリストを|g:lspoints#extensions|に追加し、
78 | 指定された拡張を読み込みます。
79 | vimrc内でも実行可能で、その場合は追加だけが行われます。
80 | 各要素は文字列で、名前が指定された場合は |'runtimepath'| より
81 | `denops/@lspoints/{path}.ts` にマッチする物が読み込まれます。
82 | フルパスが指定されている場合はそのパスから読み込まれます。
83 |
84 | *lspoints#get_clients()*
85 | lspoints#get_clients([{bufnr}])
86 | {bufnr} で示されるバッファにアタッチされているクライアントのリストを
87 | 取得します。
88 | {bufnr} が省略された場合はカレントバッファが使用されます。
89 |
90 | *lspoints#notify()*
91 | lspoints#notify({name}, {method}, [{params}])
92 | {name} で示される言語サーバーに {method} の通知を、省略可能な
93 | {params} を引数にして送信します。
94 |
95 | *lspoints#request()*
96 | lspoints#request({name}, {method}, [{params}])
97 | {name} で示される言語サーバーに {method} のリクエストを、省略可能な
98 | {params} を引数にして送信し、結果を返します。
99 |
100 | *lspoints#request_async()*
101 | lspoints#request_async({name}, {method}, [{params}])
102 | {name} で示される言語サーバーに {method} のリクエストを、{params} を
103 | 引数にして送信し、結果を引数に {success} か {failure} に渡した
104 | コールバックを呼び出します。
105 |
106 | ==============================================================================
107 | VARIABLE *lspoints-variable*
108 |
109 | *g:lspoints#extensions*
110 |
111 | |lspoints#load_extensions()|に渡す物と同じ要素からなるリストです。
112 | ここに指定した拡張はlspointsが起動する際に読み込まれます。
113 |
114 | vim:tw=78:ts=8:noet:ft=help:norl:
115 |
--------------------------------------------------------------------------------
/doc/lspoints_example.vim:
--------------------------------------------------------------------------------
1 | " see also ./config.ts
2 |
3 | " function style
4 | call lspoints#load_extensions([
5 | \ 'config',
6 | \ 'format',
7 | \ 'nvim_diagnostics',
8 | \ ])
9 | " or variable style
10 | let g:lspoints#extensions = [
11 | \ 'config',
12 | \ 'format',
13 | \ 'nvim_diagnostics',
14 | \ ]
15 | function s:post() abort
16 | call lspoints#settings#patch(#{
17 | \ tracePath: '/tmp/lspoints',
18 | \ })
19 | endfunction
20 |
21 | autocmd User DenopsPluginPost:lspoints call s:post()
22 |
23 | function s:attach_denols() abort
24 | " Vim script way to given options
25 | call lspoints#attach('denols', #{
26 | \ cmd: ['deno', 'lsp'],
27 | \ settings: #{
28 | \ deno: #{
29 | \ enable: v:true,
30 | \ unstable: v:true,
31 | \ suggest: #{
32 | \ autoImports: v:false,
33 | \ },
34 | \ },
35 | \ },
36 | \ })
37 | endfunction
38 |
39 | autocmd FileType typescript,typescriptreact call s:attach_denols()
40 |
41 | function s:on_attach() abort
42 | nnoremap mf call denops#request('lspoints', 'executeCommand', ['format', 'execute', bufnr()])
43 | endfunction
44 |
45 | autocmd User LspointsAttach:* call s:on_attach()
46 |
--------------------------------------------------------------------------------
/lua/lspoints.lua:
--------------------------------------------------------------------------------
1 | local namespaces = {}
2 |
3 | local M = {}
4 |
5 | vim.api.nvim_create_autocmd('User', {
6 | pattern = 'DenopsPluginPre:lspoints',
7 | callback = function()
8 | for _, namespace in pairs(namespaces) do
9 | vim.diagnostic.reset(namespace)
10 | end
11 | end,
12 | })
13 |
14 | function M.notify(msg)
15 | if namespaces[msg.client] == nil then
16 | namespaces[msg.client] = vim.api.nvim_create_namespace('lspoints.diagnostics.' .. msg.client)
17 | end
18 | local namespace = namespaces[msg.client]
19 | vim.diagnostic.reset(namespace, msg.bufnr)
20 | vim.diagnostic.set(namespace, msg.bufnr, msg.diagnostics)
21 | end
22 |
23 | return M
24 |
--------------------------------------------------------------------------------