├── .github └── workflows │ └── integration.yml ├── .luacheckrc ├── LICENSE ├── README.md ├── deno.jsonc ├── denops ├── @ddc-filters │ ├── converter_kind_labels.ts │ └── sorter_lsp-kind.ts ├── @ddc-sources │ └── lsp.ts └── ddc-source-lsp │ ├── client.ts │ ├── completion_item.ts │ ├── completion_item_test.ts │ ├── deps │ └── lsp.ts │ ├── request.ts │ ├── select_text.ts │ ├── select_text_test.ts │ ├── snippet.ts │ ├── snippet_test.ts │ └── test_util.ts ├── doc ├── .gitignore ├── ddc-filter-converter_kind_labels.txt ├── ddc-filter-sorter_lsp-kind.txt └── ddc-source-lsp.txt ├── lua └── ddc_source_lsp │ ├── init.lua │ └── internal.lua ├── plugin └── ddc_source_lsp.vim └── stylua.toml /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | env: 4 | DENO_VERSION: 1.x 5 | 6 | on: 7 | schedule: 8 | - cron: "0 7 * * 0" 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - "**/*.ts" 14 | - "**/*.lua" 15 | - "**/*.vim" 16 | - ".github/workflows/integration.yml" 17 | pull_request: 18 | branches: 19 | - main 20 | paths: 21 | - "**/*.ts" 22 | - "**/*.lua" 23 | - "**/*.vim" 24 | - ".github/workflows/integration.yml" 25 | 26 | jobs: 27 | # It does not work currently. 28 | # https://github.com/leafo/gh-actions-lua/issues/47 29 | #check: 30 | # runs-on: ubuntu-latest 31 | # steps: 32 | # - uses: actions/checkout@v4 33 | # - uses: denoland/setup-deno@v1 34 | # with: 35 | # deno-version: ${{ env.DENO_VERSION }} 36 | # - uses: leafo/gh-actions-lua@v10 37 | # with: 38 | # luaVersion: "luajit-2.1.0-beta3" 39 | # - uses: leafo/gh-actions-luarocks@v4 40 | # - run: luarocks install luacheck 41 | 42 | # - name: Check 43 | # run: deno task check 44 | test: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | path: "./repo" 50 | - uses: actions/checkout@v4 51 | with: 52 | repository: "vim-denops/denops.vim" 53 | path: "./denops.vim" 54 | - uses: denoland/setup-deno@v1 55 | with: 56 | deno-version: ${{ env.DENO_VERSION }} 57 | - uses: rhysd/action-setup-vim@v1 58 | with: 59 | version: nightly 60 | neovim: true 61 | - name: Test 62 | run: deno task test 63 | env: 64 | DENOPS_TEST_DENOPS_PATH: "../denops.vim" 65 | working-directory: ./repo 66 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 'vim', 'describe', 'it', 'assert', 'before_each', 'after_each' } 2 | max_line_length = false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) Shougo Matsushita , 4 | Tsuyoshi NAKAI 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddc-source-lsp 2 | 3 | lsp completion for ddc.vim 4 | 5 | ## Required 6 | 7 | ### denops.vim 8 | 9 | https://github.com/vim-denops/denops.vim 10 | 11 | ### ddc.vim 12 | 13 | https://github.com/Shougo/ddc.vim 14 | 15 | ### LSP client 16 | 17 | Supported LSP clients are "nvim-lsp", "vim-lsp" and "lspoints" 18 | 19 | https://github.com/prabirshrestha/vim-lsp 20 | 21 | https://github.com/kuuote/lspoints 22 | 23 | NOTE: If you use "nvim-lsp", it requires Neovim 0.11+. 24 | 25 | ## Configuration 26 | 27 | To take advantage of all the features, you need to set client_capabilities. 28 | 29 | ```lua 30 | vim.lsp.config('*', { 31 | capabilities = require("ddc_source_lsp").make_client_capabilities(), 32 | }) 33 | ``` 34 | 35 | ```vim 36 | call ddc#custom#patch_global('sources', ['lsp']) 37 | call ddc#custom#patch_global('sourceOptions', #{ 38 | \ lsp: #{ 39 | \ mark: 'lsp', 40 | \ forceCompletionPattern: '\.\w*|:\w*|->\w*', 41 | \ }, 42 | \ }) 43 | 44 | call ddc#custom#patch_global('sourceParams', #{ 45 | \ lsp: #{ 46 | \ snippetEngine: denops#callback#register({ 47 | \ body -> vsnip#anonymous(body) 48 | \ }), 49 | \ enableResolveItem: v:true, 50 | \ enableAdditionalTextEdit: v:true, 51 | \ } 52 | \ }) 53 | ``` 54 | 55 | ## Original code 56 | 57 | It based on [cmp-core-example](https://github.com/hrsh7th/cmp-core-example). 58 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "pre-commit": "deno task fmt && deno task check && deno task test", 5 | "test": "deno test -A", 6 | "check": "deno check **/*.ts && deno fmt --check **/*.ts && deno lint && luacheck lua/*", 7 | "fmt": "deno fmt **/*.ts && stylua lua/*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /denops/@ddc-filters/converter_kind_labels.ts: -------------------------------------------------------------------------------- 1 | import { byteLength } from "../ddc-source-lsp/completion_item.ts"; 2 | 3 | import { type Item } from "jsr:@shougo/ddc-vim@~9.4.0/types"; 4 | import { BaseFilter } from "jsr:@shougo/ddc-vim@~9.4.0/filter"; 5 | 6 | type Params = { 7 | kindLabels: Record; 8 | kindHlGroups: Record; 9 | }; 10 | 11 | export class Filter extends BaseFilter { 12 | override filter(args: { 13 | filterParams: Params; 14 | items: Item[]; 15 | }): Promise { 16 | const { kindLabels: labels, kindHlGroups: hlGroups } = args.filterParams; 17 | 18 | for (const item of args.items) { 19 | const kind = item.kind ?? ""; 20 | 21 | item.kind = labels[kind] ?? item.kind; 22 | 23 | const hl_group = hlGroups[kind]; 24 | if (!hl_group) { 25 | continue; 26 | } 27 | const hlName = `lsp-kind-label-${kind}`; 28 | if (item.highlights?.some((hl) => hl.name === hlName)) { 29 | continue; 30 | } 31 | item.highlights = [ 32 | ...item.highlights ?? [], 33 | { 34 | name: hlName, 35 | type: "kind", 36 | hl_group, 37 | col: 1, 38 | width: byteLength(kind), 39 | }, 40 | ]; 41 | } 42 | 43 | return Promise.resolve(args.items); 44 | } 45 | 46 | override params(): Params { 47 | return { 48 | kindLabels: {}, 49 | kindHlGroups: {}, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /denops/@ddc-filters/sorter_lsp-kind.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseFilter, 3 | type FilterArguments, 4 | } from "jsr:@shougo/ddc-vim@~9.4.0/filter"; 5 | import { type Item } from "jsr:@shougo/ddc-vim@~9.4.0/types"; 6 | import { CompletionItem } from "../ddc-source-lsp/completion_item.ts"; 7 | 8 | type LspKind = typeof CompletionItem.Kind[keyof typeof CompletionItem.Kind]; 9 | 10 | type Params = { 11 | priority: (LspKind | LspKind[])[]; 12 | }; 13 | 14 | export class Filter extends BaseFilter { 15 | filter({ 16 | items, 17 | filterParams, 18 | }: FilterArguments): Promise { 19 | const priorityMap: Partial> = {}; 20 | filterParams.priority.forEach((kinds, i) => { 21 | if (Array.isArray(kinds)) { 22 | kinds.forEach((kind) => priorityMap[kind] = i); 23 | } else { 24 | priorityMap[kinds] = i; 25 | } 26 | }); 27 | 28 | items.sort((a, b) => 29 | (priorityMap[(a.kind ?? "") as LspKind] ?? 100) - 30 | (priorityMap[(b.kind ?? "") as LspKind] ?? 100) 31 | ); 32 | return Promise.resolve(items); 33 | } 34 | 35 | params(): Params { 36 | const priority = Object.values(CompletionItem.Kind); 37 | // "Snippet" at the beginning 38 | priority.splice(priority.indexOf("Snippet"), 1); 39 | priority.unshift("Snippet"); 40 | // "Text" at the end 41 | priority.splice(priority.indexOf("Text"), 1); 42 | priority.push("Text"); 43 | return { priority }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /denops/@ddc-sources/lsp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LineContext, 3 | LSP, 4 | makePositionParams, 5 | OffsetEncoding, 6 | parseSnippet, 7 | } from "../ddc-source-lsp/deps/lsp.ts"; 8 | import { CompletionItem } from "../ddc-source-lsp/completion_item.ts"; 9 | import { request } from "../ddc-source-lsp/request.ts"; 10 | import { Client, getClients } from "../ddc-source-lsp/client.ts"; 11 | 12 | import { 13 | type DdcGatherItems, 14 | type Item, 15 | type Previewer, 16 | } from "jsr:@shougo/ddc-vim@~9.4.0/types"; 17 | import { 18 | BaseSource, 19 | type GatherArguments, 20 | type GetPreviewerArguments, 21 | type OnCompleteDoneArguments, 22 | } from "jsr:@shougo/ddc-vim@~9.4.0/source"; 23 | 24 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 25 | import * as fn from "jsr:@denops/std@~7.5.0/function"; 26 | import * as op from "jsr:@denops/std@~7.5.0/option"; 27 | 28 | import { ensure } from "jsr:@core/unknownutil@~4.3.0/ensure"; 29 | import { is } from "jsr:@core/unknownutil@~4.3.0/is"; 30 | 31 | type Result = LSP.CompletionList | LSP.CompletionItem[]; 32 | 33 | export type ConfirmBehavior = "insert" | "replace"; 34 | 35 | export type UserData = { 36 | lspitem: string; 37 | clientId: number | string; 38 | offsetEncoding: OffsetEncoding; 39 | resolvable: boolean; 40 | // e.g. 41 | // call getbuf 42 | lineOnRequest: string; 43 | // call getbuf| 44 | // ^ 45 | requestCharacter: number; 46 | // call |getbuf 47 | // ^ 48 | suggestCharacter: number; 49 | }; 50 | 51 | export type Params = { 52 | confirmBehavior: ConfirmBehavior; 53 | enableDisplayDetail: boolean; 54 | enableMatchLabel: boolean; 55 | enableResolveItem: boolean; 56 | enableAdditionalTextEdit: boolean; 57 | lspEngine: "nvim-lsp" | "vim-lsp" | "lspoints"; 58 | manualOnlyServers: string[]; 59 | snippetEngine: 60 | | string // ID of denops#callback. 61 | | ((body: string) => Promise); 62 | snippetIndicator: string; 63 | bufnr?: number; 64 | }; 65 | 66 | function isDefined(x: T | undefined): x is T { 67 | return x !== undefined; 68 | } 69 | 70 | function splitLines(str: string): string[] { 71 | return str.replaceAll(/\r\n?/g, "\n").split("\n"); 72 | } 73 | 74 | export class Source extends BaseSource { 75 | #item_cache: Record[]> = {}; 76 | 77 | override async gather( 78 | args: GatherArguments, 79 | ): Promise> { 80 | const denops = args.denops; 81 | 82 | if (denops.meta.host === "nvim" && !await fn.has(denops, "nvim-0.11")) { 83 | this.#printError(denops, "ddc-source-lsp requires Neovim 0.11+."); 84 | return []; 85 | } 86 | 87 | const lineOnRequest = await fn.getline(denops, "."); 88 | let isIncomplete = false; 89 | const cursorLine = (await fn.line(denops, ".")) - 1; 90 | 91 | const clients = (await getClients( 92 | denops, 93 | args.sourceParams.lspEngine, 94 | args.sourceParams.bufnr, 95 | ).catch(() => [])).filter((client) => 96 | args.context.event === "Manual" || 97 | !args.sourceParams.manualOnlyServers.includes(client.name) 98 | ); 99 | 100 | const items = await Promise.all(clients.map(async (client) => { 101 | if (this.#item_cache[client.id]) { 102 | return this.#item_cache[client.id]; 103 | } 104 | 105 | const result = await this.#request(denops, client, args); 106 | if (!result) { 107 | return []; 108 | } 109 | 110 | const completionItem = new CompletionItem( 111 | client.id, 112 | client.offsetEncoding, 113 | client.provider.resolveProvider === true, 114 | lineOnRequest, 115 | args.completePos, 116 | args.completePos + args.completeStr.length, 117 | cursorLine, 118 | args.sourceParams.snippetIndicator, 119 | ); 120 | 121 | const completionList = Array.isArray(result) 122 | ? { items: result, isIncomplete: false } 123 | : result; 124 | const items = completionList.items.map((lspItem: LSP.CompletionItem) => 125 | completionItem.toDdcItem( 126 | lspItem, 127 | completionList.itemDefaults, 128 | args.sourceParams.enableDisplayDetail, 129 | args.sourceParams.enableMatchLabel, 130 | ) 131 | ).filter(isDefined); 132 | if (!completionList.isIncomplete) { 133 | this.#item_cache[client.id] = items; 134 | } 135 | isIncomplete = isIncomplete || completionList.isIncomplete; 136 | 137 | return items; 138 | })).then((items) => items.flat(1)) 139 | .catch((e) => { 140 | this.#printError(denops, e); 141 | return []; 142 | }); 143 | 144 | if (!isIncomplete) { 145 | this.#item_cache = {}; 146 | } 147 | 148 | return { 149 | items, 150 | isIncomplete, 151 | }; 152 | } 153 | 154 | async #request( 155 | denops: Denops, 156 | client: Client, 157 | args: GatherArguments, 158 | ): Promise { 159 | const params = await makePositionParams( 160 | denops, 161 | args.sourceParams.bufnr, 162 | undefined, 163 | client.offsetEncoding, 164 | ) as LSP.CompletionParams; 165 | const trigger = args.context.input.slice(-1); 166 | if (client.provider.triggerCharacters?.includes(trigger)) { 167 | params.context = { 168 | triggerKind: LSP.CompletionTriggerKind.TriggerCharacter, 169 | triggerCharacter: trigger, 170 | }; 171 | } else { 172 | params.context = { 173 | triggerKind: args.isIncomplete 174 | ? LSP.CompletionTriggerKind.TriggerForIncompleteCompletions 175 | : LSP.CompletionTriggerKind.Invoked, 176 | }; 177 | } 178 | 179 | try { 180 | return await request( 181 | denops, 182 | args.sourceParams.lspEngine, 183 | "textDocument/completion", 184 | params, 185 | { 186 | client, 187 | timeout: args.sourceOptions.timeout, 188 | sync: false, 189 | bufnr: args.sourceParams.bufnr, 190 | }, 191 | ) as Result; 192 | } catch (e) { 193 | if (!(e instanceof DOMException)) { 194 | throw e; 195 | } 196 | } 197 | } 198 | 199 | async #printError( 200 | denops: Denops, 201 | message: Error | string, 202 | ) { 203 | await denops.call( 204 | `ddc#util#print_error`, 205 | message.toString(), 206 | "ddc-source-lsp", 207 | ); 208 | } 209 | 210 | override async onCompleteDone({ 211 | denops, 212 | userData, 213 | sourceParams: params, 214 | }: OnCompleteDoneArguments): Promise { 215 | // No expansion unless confirmed by pum#map#confirm() or complete_CTRL-Y 216 | // (native confirm) 217 | const itemWord = await denops.eval(`v:completed_item.word`) as string; 218 | const ctx = await LineContext.create(denops); 219 | if (ctx.text.slice(userData.suggestCharacter, ctx.character) !== itemWord) { 220 | return; 221 | } 222 | 223 | const unresolvedItem = JSON.parse(userData.lspitem) as LSP.CompletionItem; 224 | const lspItem = params.enableResolveItem 225 | ? await this.#resolve( 226 | denops, 227 | params.lspEngine, 228 | userData.clientId, 229 | unresolvedItem, 230 | ) 231 | : unresolvedItem; 232 | 233 | // If item.word is sufficient, do not confirm() 234 | if ( 235 | CompletionItem.getInsertText(lspItem) !== itemWord || 236 | (params.enableAdditionalTextEdit && 237 | lspItem.additionalTextEdits) || 238 | CompletionItem.isReplace( 239 | lspItem, 240 | params.confirmBehavior, 241 | userData.suggestCharacter, 242 | userData.requestCharacter, 243 | ) 244 | ) { 245 | // Set undo point 246 | // :h undo-break 247 | await denops.cmd(`let &undolevels = &undolevels`); 248 | 249 | await CompletionItem.confirm( 250 | denops, 251 | lspItem, 252 | unresolvedItem, 253 | userData, 254 | params, 255 | ); 256 | 257 | await denops.call("ddc#skip_next_complete"); 258 | } 259 | } 260 | 261 | async #resolve( 262 | denops: Denops, 263 | lspEngine: Params["lspEngine"], 264 | clientId: number | string, 265 | lspItem: LSP.CompletionItem, 266 | bufnr?: number, 267 | ): Promise { 268 | const clients = await getClients(denops, lspEngine, bufnr); 269 | const client = clients.find((c) => c.id === clientId); 270 | if (!client?.provider.resolveProvider) { 271 | return lspItem; 272 | } 273 | try { 274 | const response = await request( 275 | denops, 276 | lspEngine, 277 | "completionItem/resolve", 278 | lspItem, 279 | { client, timeout: 1000, sync: true, bufnr: bufnr }, 280 | ); 281 | const result = ensure( 282 | response, 283 | is.ObjectOf({ label: is.String }), 284 | ); 285 | return result as LSP.CompletionItem; 286 | } catch { 287 | return lspItem; 288 | } 289 | } 290 | 291 | override async getPreviewer({ 292 | denops, 293 | sourceParams: params, 294 | item, 295 | }: GetPreviewerArguments): Promise { 296 | const userData = item.user_data; 297 | if (userData === undefined) { 298 | return { kind: "empty" }; 299 | } 300 | const unresolvedItem = JSON.parse(userData.lspitem) as LSP.CompletionItem; 301 | const lspItem = await this.#resolve( 302 | denops, 303 | params.lspEngine, 304 | userData.clientId, 305 | unresolvedItem, 306 | params.bufnr, 307 | ); 308 | const filetype = await op.filetype.get(denops); 309 | const contents: string[] = []; 310 | 311 | // snippet 312 | if (lspItem.kind === 15) { 313 | const insertText = CompletionItem.getInsertText(lspItem); 314 | const body = parseSnippet(insertText); 315 | return { 316 | kind: "markdown", 317 | contents: [ 318 | "```" + filetype, 319 | ...body.replaceAll(/\r\n?/g, "\n").split("\n"), 320 | "```", 321 | ], 322 | }; 323 | } 324 | 325 | // detail 326 | if (lspItem.detail) { 327 | contents.push( 328 | "```" + filetype, 329 | ...splitLines(lspItem.detail), 330 | "```", 331 | ); 332 | } 333 | 334 | // import from (denols) 335 | if ( 336 | is.ObjectOf({ 337 | tsc: is.ObjectOf({ 338 | source: is.String, 339 | }), 340 | })(unresolvedItem.data) 341 | ) { 342 | if (contents.length > 0) { 343 | contents.push("---"); 344 | } 345 | contents.push(`import from \`${unresolvedItem.data.tsc.source}\``); 346 | } 347 | 348 | // documentation 349 | if ( 350 | (typeof lspItem.documentation === "string" && 351 | lspItem.documentation.length > 0) || 352 | (typeof lspItem.documentation === "object" && 353 | lspItem.documentation.value.length > 0) 354 | ) { 355 | if (contents.length > 0) { 356 | contents.push("---"); 357 | } 358 | contents.push(...this.converter(lspItem.documentation)); 359 | } 360 | 361 | return { kind: "markdown", contents }; 362 | } 363 | 364 | converter(doc: string | LSP.MarkupContent): string[] { 365 | if (typeof doc === "string") { 366 | return splitLines(doc); 367 | } else { 368 | const value = doc.kind === LSP.MarkupKind.PlainText 369 | ? `\n${doc.value}\n` 370 | : doc.value ?? ""; 371 | return splitLines(value); 372 | } 373 | } 374 | 375 | override params(): Params { 376 | return { 377 | confirmBehavior: "insert", 378 | enableAdditionalTextEdit: false, 379 | enableDisplayDetail: false, 380 | enableMatchLabel: false, 381 | enableResolveItem: false, 382 | lspEngine: "nvim-lsp", 383 | manualOnlyServers: [], 384 | snippetEngine: "", 385 | snippetIndicator: "~", 386 | }; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/client.ts: -------------------------------------------------------------------------------- 1 | import { OffsetEncoding } from "./deps/lsp.ts"; 2 | import { LSP } from "./deps/lsp.ts"; 3 | import { Params } from "../@ddc-sources/lsp.ts"; 4 | 5 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 6 | import * as fn from "jsr:@denops/std@~7.5.0/function"; 7 | 8 | export type Client = { 9 | id: number | string; 10 | name: string; 11 | provider: Exclude; 12 | offsetEncoding: OffsetEncoding; 13 | }; 14 | 15 | export async function getClients( 16 | denops: Denops, 17 | lspEngine: Params["lspEngine"], 18 | bufnr?: number, 19 | ): Promise { 20 | if (lspEngine === "nvim-lsp") { 21 | return await denops.call( 22 | "luaeval", 23 | `require("ddc_source_lsp.internal").get_clients(_A[1])`, 24 | [bufnr], 25 | ) as Client[]; 26 | } else if (lspEngine === "vim-lsp") { 27 | const servers = await denops.call( 28 | "lsp#get_allowed_servers", 29 | bufnr ?? await fn.bufnr(denops), 30 | ) as string[]; 31 | const clients: Client[] = []; 32 | for (const server of servers) { 33 | const serverCapabilities = await denops.call( 34 | "lsp#get_server_capabilities", 35 | server, 36 | ) as LSP.ServerCapabilities; 37 | if (serverCapabilities.completionProvider == null) { 38 | continue; 39 | } 40 | clients.push({ 41 | id: server, 42 | name: server, 43 | provider: serverCapabilities.completionProvider, 44 | offsetEncoding: serverCapabilities.positionEncoding as OffsetEncoding ?? 45 | "utf-16", 46 | }); 47 | } 48 | return clients; 49 | } else if (lspEngine === "lspoints") { 50 | return (await denops.dispatch( 51 | "lspoints", 52 | "getClients", 53 | bufnr ?? await fn.bufnr(denops), 54 | ) as { 55 | id: number; 56 | name: string; 57 | serverCapabilities: LSP.ServerCapabilities; 58 | }[]) 59 | .filter((c) => c.serverCapabilities.completionProvider != null) 60 | .map((c): Client => ({ 61 | id: c.id, 62 | name: c.name, 63 | provider: c.serverCapabilities.completionProvider!, 64 | offsetEncoding: 65 | c.serverCapabilities.positionEncoding as OffsetEncoding ?? "utf-16", 66 | })); 67 | } else { 68 | lspEngine satisfies never; 69 | throw new Error(`Unknown lspEngine: ${lspEngine}`); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/completion_item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyTextEdits, 3 | getCursor, 4 | isPositionBefore, 5 | LineContext, 6 | linePatch, 7 | LSP, 8 | OffsetEncoding, 9 | toUtf16Index, 10 | } from "./deps/lsp.ts"; 11 | import createSelectText from "./select_text.ts"; 12 | import { ConfirmBehavior, Params, UserData } from "../@ddc-sources/lsp.ts"; 13 | import * as snippet from "./snippet.ts"; 14 | 15 | import { type Item, type PumHighlight } from "jsr:@shougo/ddc-vim@~9.4.0/types"; 16 | 17 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 18 | 19 | export class CompletionItem { 20 | static Kind = { 21 | 1: "Text", 22 | 2: "Method", 23 | 3: "Function", 24 | 4: "Constructor", 25 | 5: "Field", 26 | 6: "Variable", 27 | 7: "Class", 28 | 8: "Interface", 29 | 9: "Module", 30 | 10: "Property", 31 | 11: "Unit", 32 | 12: "Value", 33 | 13: "Enum", 34 | 14: "Keyword", 35 | 15: "Snippet", 36 | 16: "Color", 37 | 17: "File", 38 | 18: "Reference", 39 | 19: "Folder", 40 | 20: "EnumMember", 41 | 21: "Constant", 42 | 22: "Struct", 43 | 23: "Event", 44 | 24: "Operator", 45 | 25: "TypeParameter", 46 | } as const satisfies Record; 47 | 48 | #clientId: number | string; 49 | #offsetEncoding: OffsetEncoding; 50 | #resolvable: boolean; 51 | #lineOnRequest: string; 52 | #suggestCharacter: number; 53 | #requestCharacter: number; 54 | #cursorLine: number; 55 | #snippetIndicator: string; 56 | 57 | static isSnippet( 58 | lspItem: LSP.CompletionItem, 59 | ): boolean { 60 | return lspItem.insertTextFormat === LSP.InsertTextFormat.Snippet; 61 | } 62 | 63 | static getInsertText( 64 | lspItem: LSP.CompletionItem, 65 | ): string { 66 | return lspItem.textEdit?.newText ?? 67 | lspItem.insertText ?? 68 | lspItem.label; 69 | } 70 | 71 | static isReplace( 72 | lspItem: LSP.CompletionItem, 73 | confirmBehavior: ConfirmBehavior, 74 | suggestCharacter: number, 75 | requestCharacter: number, 76 | ): boolean { 77 | const textEdit = lspItem.textEdit; 78 | if (!textEdit) { 79 | return false; 80 | } 81 | const range = "range" in textEdit 82 | ? textEdit.range 83 | : textEdit[confirmBehavior]; 84 | return range.start.character < suggestCharacter || 85 | range.end.character > requestCharacter; 86 | } 87 | 88 | static async confirm( 89 | denops: Denops, 90 | lspItem: LSP.CompletionItem, 91 | unresolvedItem: LSP.CompletionItem, 92 | userData: UserData, 93 | params: Params, 94 | ): Promise { 95 | // Restore the requested state 96 | let ctx = await LineContext.create(denops); 97 | await linePatch( 98 | denops, 99 | ctx.character - userData.suggestCharacter, 100 | 0, 101 | userData.lineOnRequest.slice( 102 | userData.suggestCharacter, 103 | userData.requestCharacter, 104 | ), 105 | ); 106 | 107 | ctx = await LineContext.create(denops); 108 | let before: number, after: number; 109 | if (!lspItem.textEdit) { 110 | before = ctx.character - userData.suggestCharacter; 111 | after = 0; 112 | } else { 113 | const range = "range" in lspItem.textEdit 114 | ? lspItem.textEdit.range 115 | : lspItem.textEdit[params.confirmBehavior]; 116 | before = ctx.character - range.start.character; 117 | after = range.end.character - ctx.character; 118 | } 119 | 120 | // Apply sync additionalTextEdits 121 | if (params.enableAdditionalTextEdit && unresolvedItem.additionalTextEdits) { 122 | await applyTextEdits( 123 | denops, 124 | 0, 125 | unresolvedItem.additionalTextEdits, 126 | userData.offsetEncoding, 127 | ); 128 | } 129 | 130 | // Expand main part 131 | const insertText = this.getInsertText(lspItem); 132 | if (this.isSnippet(lspItem)) { 133 | await linePatch(denops, before, after, ""); 134 | await snippet.expand(denops, insertText, params.snippetEngine); 135 | } else { 136 | await linePatch(denops, before, after, insertText); 137 | } 138 | 139 | // Apply async additionalTextEdits 140 | if ( 141 | params.enableResolveItem && 142 | (!unresolvedItem.additionalTextEdits || 143 | unresolvedItem.additionalTextEdits.length === 0) && 144 | lspItem.additionalTextEdits 145 | ) { 146 | const cursor = await getCursor(denops); 147 | if ( 148 | !lspItem.additionalTextEdits.some((edit: LSP.TextEdit) => 149 | isPositionBefore(cursor, edit.range.start) 150 | ) 151 | ) { 152 | await applyTextEdits( 153 | denops, 154 | 0, 155 | lspItem.additionalTextEdits, 156 | userData.offsetEncoding, 157 | ); 158 | } 159 | } 160 | 161 | // Execute command 162 | if (lspItem.command) { 163 | await denops.call( 164 | "luaeval", 165 | `require("ddc_source_lsp.internal").execute(_A[1], _A[2])`, 166 | [userData.clientId, lspItem.command], 167 | ); 168 | } 169 | } 170 | 171 | constructor( 172 | clientId: number | string, 173 | offsetEncoding: OffsetEncoding, 174 | resolvable: boolean, 175 | lineOnRequest: string, 176 | suggestCharacter: number, 177 | requestCharacter: number, 178 | cursorLine: number, 179 | snippetIndicator: string, 180 | ) { 181 | this.#clientId = clientId; 182 | this.#offsetEncoding = offsetEncoding; 183 | this.#resolvable = resolvable; 184 | this.#lineOnRequest = lineOnRequest; 185 | this.#suggestCharacter = suggestCharacter; 186 | this.#requestCharacter = requestCharacter; 187 | this.#cursorLine = cursorLine; 188 | this.#snippetIndicator = snippetIndicator; 189 | } 190 | 191 | toDdcItem( 192 | lspItem: LSP.CompletionItem, 193 | defaults?: LSP.CompletionList["itemDefaults"], 194 | enableDisplayDetail?: boolean, 195 | enableMatchLabel?: boolean, 196 | ): Item | undefined { 197 | lspItem = this.#fillDefaults(lspItem, defaults); 198 | 199 | let isInvalid = false; 200 | // validate label 201 | isInvalid = isInvalid || !lspItem.label; 202 | // validate range 203 | if (lspItem.textEdit) { 204 | const range = "range" in lspItem.textEdit 205 | ? lspItem.textEdit.range 206 | : lspItem.textEdit.insert; 207 | isInvalid = isInvalid || range.start.line !== range.end.line || 208 | range.start.line !== this.#cursorLine; 209 | } 210 | if (isInvalid) { 211 | return; 212 | } 213 | 214 | const word = this.#getWord(lspItem); 215 | if (enableMatchLabel && lspItem.label !== word) { 216 | return; 217 | } 218 | 219 | const { abbr, highlights } = this.#getAbbr(lspItem); 220 | const index: keyof typeof CompletionItem.Kind = lspItem.kind ?? 1; 221 | return { 222 | word: createSelectText(word), 223 | abbr, 224 | kind: CompletionItem.Kind[index], 225 | menu: enableDisplayDetail ? (lspItem.detail ?? "") : "", 226 | highlights, 227 | user_data: { 228 | lspitem: JSON.stringify(lspItem), 229 | clientId: this.#clientId, 230 | offsetEncoding: this.#offsetEncoding, 231 | resolvable: this.#resolvable, 232 | lineOnRequest: this.#lineOnRequest, 233 | suggestCharacter: this.#suggestCharacter, 234 | requestCharacter: this.#requestCharacter, 235 | }, 236 | }; 237 | } 238 | 239 | #fillDefaults( 240 | lspItem: LSP.CompletionItem, 241 | defaults?: LSP.CompletionList["itemDefaults"], 242 | ): LSP.CompletionItem { 243 | if (!defaults) { 244 | return lspItem; 245 | } 246 | 247 | if (defaults.editRange && !lspItem.textEdit) { 248 | if ("insert" in defaults.editRange) { 249 | lspItem.textEdit = { 250 | ...defaults.editRange, 251 | newText: lspItem.textEditText ?? lspItem.label, 252 | }; 253 | } else { 254 | lspItem.textEdit = { 255 | range: defaults.editRange, 256 | newText: lspItem.textEditText ?? lspItem.label, 257 | }; 258 | } 259 | } 260 | 261 | const filledItem = { 262 | ...defaults, 263 | ...lspItem, 264 | }; 265 | delete filledItem.editRange; 266 | 267 | return filledItem; 268 | } 269 | 270 | #getWord( 271 | lspItem: LSP.CompletionItem, 272 | ): string { 273 | // NOTE: Use label instead of filterText 274 | // Because filterText is used for filtering 275 | // For example: 276 | // label = "read_dir()" 277 | // filterText = "read_dirls" 278 | 279 | // NOTE: Use insertText instead of label 280 | // Because label is used for display 281 | // For example: 282 | // label = "•atan2" 283 | // insertText = "atan2" 284 | 285 | const text = lspItem.insertText ?? lspItem.label.trim(); 286 | 287 | const defaultOffset = this.#suggestCharacter; 288 | let offset = this.#getOffset(lspItem, defaultOffset); 289 | if (offset < defaultOffset) { 290 | const prefix = this.#lineOnRequest.slice(offset, defaultOffset); 291 | if (!text.startsWith(prefix)) { 292 | offset = defaultOffset; 293 | } 294 | } 295 | const fixedLine = this.#lineOnRequest.slice(0, offset) + text; 296 | return fixedLine.slice(defaultOffset); 297 | } 298 | 299 | #getOffset( 300 | lspItem: LSP.CompletionItem, 301 | defaultOffset: number, 302 | ): number { 303 | const textEdit = lspItem.textEdit; 304 | if (textEdit === undefined) { 305 | return defaultOffset; 306 | } 307 | const range = "range" in textEdit ? textEdit.range : textEdit.insert; 308 | const character = range.start.character; 309 | const offset = toUtf16Index( 310 | this.#lineOnRequest, 311 | character, 312 | this.#offsetEncoding, 313 | ); 314 | const delta = this.#lineOnRequest.slice(offset, defaultOffset).search(/\S/); 315 | return offset + (delta > 0 ? delta : 0); 316 | } 317 | 318 | #getAbbr( 319 | lspItem: LSP.CompletionItem, 320 | ): { abbr: string; highlights?: PumHighlight[] } { 321 | const abbr = lspItem.insertTextFormat === LSP.InsertTextFormat.Snippet 322 | ? `${lspItem.label}${this.#snippetIndicator}` 323 | : lspItem.label; 324 | return { 325 | abbr, 326 | highlights: this.#isDeprecated(lspItem) 327 | ? [{ 328 | type: "abbr", 329 | // NOTE: The property 'name' only makes sense in Vim. 330 | name: `ddc-source-lsp-deprecated`, 331 | hl_group: "DdcLspDeprecated", 332 | col: 1, 333 | width: byteLength(abbr), 334 | }] 335 | : [], 336 | }; 337 | } 338 | 339 | #isDeprecated( 340 | lspItem: LSP.CompletionItem, 341 | ): boolean { 342 | return lspItem.deprecated || 343 | !!lspItem.tags?.includes(LSP.CompletionItemTag.Deprecated); 344 | } 345 | } 346 | 347 | const ENCODER = new TextEncoder(); 348 | export function byteLength( 349 | s: string, 350 | ): number { 351 | return ENCODER.encode(s).length; 352 | } 353 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/completion_item_test.ts: -------------------------------------------------------------------------------- 1 | import { LSP, type OffsetEncoding } from "./deps/lsp.ts"; 2 | import { assertBuffer, searchCursor } from "./test_util.ts"; 3 | import { CompletionItem } from "./completion_item.ts"; 4 | import { Params } from "../@ddc-sources/lsp.ts"; 5 | 6 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 7 | import * as nvim from "jsr:@denops/std@~7.5.0/function/nvim"; 8 | import { test } from "jsr:@denops/test@~3.0.2"; 9 | 10 | import { assertEquals } from "jsr:@std/assert@~1.0.0/equals"; 11 | 12 | const params: Params = { 13 | confirmBehavior: "insert", 14 | enableAdditionalTextEdit: true, 15 | enableDisplayDetail: false, 16 | enableMatchLabel: false, 17 | enableResolveItem: false, 18 | lspEngine: "nvim-lsp", 19 | manualOnlyServers: [], 20 | snippetEngine: "", 21 | snippetIndicator: "~", 22 | }; 23 | 24 | const ClientId = 0 as const satisfies number; 25 | const OffsetEncoding = "utf-16" as const satisfies OffsetEncoding; 26 | const Resolvable = false as const satisfies boolean; 27 | 28 | async function setup(args: { 29 | denops: Denops; 30 | input: string; 31 | buffer: string[]; 32 | lspItem: LSP.CompletionItem; 33 | }) { 34 | const { row, col, completePos } = searchCursor(args.buffer, args.input); 35 | await nvim.nvim_buf_set_lines(args.denops, 0, 0, -1, true, args.buffer); 36 | await nvim.nvim_win_set_cursor(args.denops, 0, [row, col]); 37 | 38 | const completionItem = new CompletionItem( 39 | ClientId, 40 | OffsetEncoding, 41 | Resolvable, 42 | args.buffer[row - 1], 43 | completePos, 44 | completePos + args.input.length, 45 | row - 1, 46 | "~", 47 | ); 48 | const ddcItem = completionItem.toDdcItem(args.lspItem); 49 | if (ddcItem === undefined) { 50 | throw new Error("Protocol violations"); 51 | } 52 | return ddcItem; 53 | } 54 | 55 | function makeRange( 56 | sl: number, 57 | sc: number, 58 | el: number, 59 | ec: number, 60 | ): LSP.Range { 61 | return { 62 | start: { line: sl, character: sc }, 63 | end: { line: el, character: ec }, 64 | }; 65 | } 66 | 67 | test({ 68 | name: "indent fixing completion (vscode-html-language-server)", 69 | mode: "nvim", 70 | fn: async (denops) => { 71 | const lspItem = { 72 | label: "/div", 73 | filterText: " ", 84 | " ", 85 | ], 86 | lspItem, 87 | }); 88 | 89 | assertEquals(ddcItem.word, "/div"); 90 | assertEquals(ddcItem.abbr, "/div"); 91 | 92 | await CompletionItem.confirm( 93 | denops, 94 | lspItem, 95 | lspItem, 96 | ddcItem.user_data!, 97 | params, 98 | ); 99 | 100 | await assertBuffer(denops, [ 101 | "
", 102 | "", 103 | ]); 104 | }, 105 | }); 106 | 107 | test({ 108 | name: "dot-to-arrow completion (clangd)", 109 | mode: "nvim", 110 | fn: async (denops) => { 111 | const lspItem = { 112 | label: " prop", 113 | filterText: "prop", 114 | textEdit: { 115 | range: makeRange(0, 3, 0, 5), 116 | newText: "->prop", 117 | }, 118 | } satisfies LSP.CompletionItem; 119 | const ddcItem = await setup({ 120 | denops, 121 | input: "p", 122 | buffer: [ 123 | "obj.|foo", 124 | ], 125 | lspItem, 126 | }); 127 | 128 | assertEquals(ddcItem.word, "prop"); 129 | assertEquals(ddcItem.abbr, " prop"); 130 | 131 | await CompletionItem.confirm( 132 | denops, 133 | lspItem, 134 | lspItem, 135 | ddcItem.user_data!, 136 | params, 137 | ); 138 | 139 | await assertBuffer(denops, ["obj->prop|foo"]); 140 | }, 141 | }); 142 | 143 | test({ 144 | name: "symbol reference completion (typescript-language-server)", 145 | mode: "nvim", 146 | fn: async (denops) => { 147 | const lspItem = { 148 | label: "Symbol", 149 | filterText: ".Symbol", 150 | textEdit: { 151 | range: makeRange(0, 2, 0, 4), 152 | newText: "[Symbol]", 153 | }, 154 | } satisfies LSP.CompletionItem; 155 | const ddcItem = await setup({ 156 | denops, 157 | input: "S", 158 | buffer: [ 159 | "[].|foo", 160 | ], 161 | lspItem, 162 | }); 163 | 164 | assertEquals(ddcItem.word, "Symbol"); 165 | assertEquals(ddcItem.abbr, "Symbol"); 166 | 167 | await CompletionItem.confirm( 168 | denops, 169 | lspItem, 170 | lspItem, 171 | ddcItem.user_data!, 172 | params, 173 | ); 174 | 175 | await assertBuffer(denops, ["[][Symbol]|foo"]); 176 | }, 177 | }); 178 | 179 | test({ 180 | name: "extreme additionalTextEdits completion (rust-analyzer)", 181 | mode: "nvim", 182 | fn: async (denops) => { 183 | const lspItem = { 184 | label: "dbg", 185 | filterText: "dbg", 186 | textEdit: { 187 | insert: makeRange(2, 5, 2, 6), 188 | replace: makeRange(2, 5, 2, 9), 189 | newText: `dbg!("")`, 190 | }, 191 | additionalTextEdits: [{ 192 | range: makeRange(1, 10, 2, 5), 193 | newText: "", 194 | }], 195 | } satisfies LSP.CompletionItem; 196 | const ddcItem = await setup({ 197 | denops, 198 | input: "d", 199 | buffer: [ 200 | "fn main() {", 201 | ` let s = ""`, 202 | " .|foo", 203 | "}", 204 | ], 205 | lspItem, 206 | }); 207 | 208 | assertEquals(ddcItem.word, "dbg"); 209 | assertEquals(ddcItem.abbr, "dbg"); 210 | 211 | await CompletionItem.confirm( 212 | denops, 213 | lspItem, 214 | lspItem, 215 | ddcItem.user_data!, 216 | params, 217 | ); 218 | 219 | await assertBuffer(denops, [ 220 | "fn main() {", 221 | ` let s = dbg!("")|foo`, 222 | "}", 223 | ]); 224 | }, 225 | }); 226 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/deps/lsp.ts: -------------------------------------------------------------------------------- 1 | export { 2 | applyTextEdits, 3 | getCursor, 4 | isPositionBefore, 5 | LineContext, 6 | linePatch, 7 | makePositionParams, 8 | type OffsetEncoding, 9 | parseSnippet, 10 | toUtf16Index, 11 | uriFromBufnr, 12 | } from "jsr:@uga-rosa/denops-lsputil@~0.10.1"; 13 | 14 | export * as LSP from "npm:vscode-languageserver-protocol@~3.17.5"; 15 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/request.ts: -------------------------------------------------------------------------------- 1 | import { uriFromBufnr } from "./deps/lsp.ts"; 2 | 3 | import { Params } from "../@ddc-sources/lsp.ts"; 4 | import { Client } from "./client.ts"; 5 | 6 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 7 | import * as fn from "jsr:@denops/std@~7.5.0/function"; 8 | import { register } from "jsr:@denops/std@~7.5.0/lambda"; 9 | 10 | import { deadline } from "jsr:@std/async@~1.0.0/deadline"; 11 | import { ensure } from "jsr:@core/unknownutil@~4.3.0/ensure"; 12 | import { is } from "jsr:@core/unknownutil@~4.3.0/is"; 13 | 14 | export async function request( 15 | denops: Denops, 16 | lspEngine: Params["lspEngine"], 17 | method: string, 18 | params: unknown, 19 | opts: { client: Client; timeout: number; sync: boolean; bufnr?: number }, 20 | ): Promise { 21 | if (lspEngine === "nvim-lsp") { 22 | if (opts.sync) { 23 | return await denops.call( 24 | `luaeval`, 25 | `require("ddc_source_lsp.internal").request_sync(_A[1], _A[2], _A[3], _A[4])`, 26 | [ 27 | opts.client.id, 28 | method, 29 | params, 30 | { timemout: opts.timeout, bufnr: opts.bufnr }, 31 | ], 32 | ); 33 | } else { 34 | const waiter = Promise.withResolvers(); 35 | const lambda_id = register( 36 | denops, 37 | (res: unknown) => waiter.resolve(res), 38 | { once: true }, 39 | ); 40 | await denops.call( 41 | `luaeval`, 42 | `require("ddc_source_lsp.internal").request(_A[1], _A[2], _A[3], _A[4])`, 43 | [opts.client.id, method, params, { 44 | plugin_name: denops.name, 45 | lambda_id, 46 | bufnr: opts.bufnr, 47 | }], 48 | ); 49 | return deadline(waiter.promise, opts.timeout); 50 | } 51 | } else if (lspEngine === "vim-lsp") { 52 | const waiter = Promise.withResolvers(); 53 | const id = register( 54 | denops, 55 | (res: unknown) => waiter.resolve(res), 56 | { once: true }, 57 | ); 58 | try { 59 | await denops.eval( 60 | `lsp#send_request(l:server, extend(l:request,` + 61 | `{'on_notification': {data -> denops#notify(l:name, l:id, [data])}}))`, 62 | { 63 | server: opts.client.id, 64 | request: { method, params }, 65 | name: denops.name, 66 | id, 67 | bufnr: opts.bufnr ?? await fn.bufnr(denops), 68 | }, 69 | ); 70 | const resolvedData = await deadline(waiter.promise, opts.timeout); 71 | const { response: { result } } = ensure( 72 | resolvedData, 73 | is.ObjectOf({ response: is.ObjectOf({ result: is.Any }) }), 74 | ); 75 | return result; 76 | } catch (e) { 77 | if (e instanceof DOMException) { 78 | throw new Error(`No response from server ${opts.client.id}`); 79 | } else { 80 | throw new Error(`Unsupported method: ${method}`); 81 | } 82 | } 83 | } else if (lspEngine === "lspoints") { 84 | if (opts.bufnr != null && opts.bufnr > 0 && is.Record(params)) { 85 | return await denops.dispatch( 86 | "lspoints", 87 | "request", 88 | opts.client.id, 89 | method, 90 | { 91 | ...params, 92 | textDocument: { uri: await uriFromBufnr(denops, opts.bufnr) }, 93 | }, 94 | ); 95 | } 96 | return await denops.dispatch( 97 | "lspoints", 98 | "request", 99 | opts.client.id, 100 | method, 101 | params, 102 | ); 103 | } else { 104 | lspEngine satisfies never; 105 | throw new Error(`unknown lspEngine: ${lspEngine}`); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/select_text.ts: -------------------------------------------------------------------------------- 1 | export default function createSelectText( 2 | insertText: string, 3 | ): string { 4 | let is_alnum_consumed = false; 5 | const pairs_stack: string[] = []; 6 | for (let i = 0; i < insertText.length; i++) { 7 | const char = insertText[i]; 8 | const alnum = is_alnum(char); 9 | 10 | const pairChar = Pairs.get(char); 11 | if (!is_alnum_consumed && pairChar) { 12 | pairs_stack.push(pairChar); 13 | } 14 | if (is_alnum_consumed && !alnum && pairs_stack.length === 0) { 15 | if (StopCharacters.has(char)) { 16 | return insertText.slice(0, i); 17 | } 18 | } else { 19 | is_alnum_consumed = is_alnum_consumed || alnum; 20 | } 21 | 22 | if (char === pairs_stack[pairs_stack.length - 1]) { 23 | pairs_stack.pop(); 24 | } 25 | } 26 | return insertText; 27 | } 28 | 29 | function is_alnum(char: string): boolean { 30 | const code = char.charCodeAt(0); 31 | return (code >= 48 && code <= 57) || // 0-9 32 | (code >= 65 && code <= 90) || // A-Z 33 | (code >= 97 && code <= 122); // a-z 34 | } 35 | 36 | const Pairs = new Map([ 37 | ["(", ")"], 38 | ["[", "]"], 39 | ["{", "}"], 40 | ['"', '"'], 41 | ["'", "'"], 42 | ["<", ">"], 43 | ]); 44 | 45 | const StopCharacters = new Set([ 46 | "'", 47 | '"', 48 | "=", 49 | "$", 50 | "(", 51 | ")", 52 | "[", 53 | "]", 54 | "<", 55 | ">", 56 | "{", 57 | "}", 58 | " ", 59 | "\t", 60 | "\n", 61 | "\r", 62 | ]); 63 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/select_text_test.ts: -------------------------------------------------------------------------------- 1 | import createSelectText from "./select_text.ts"; 2 | 3 | import { assertEquals } from "jsr:@std/assert@~1.0.0/equals"; 4 | 5 | Deno.test({ 6 | name: "parse snippet", 7 | fn: () => { 8 | const text = `function $1($2)\n\t$0\nend`; 9 | assertEquals(createSelectText(text), `function`); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/snippet.ts: -------------------------------------------------------------------------------- 1 | import { linePatch, parseSnippet } from "./deps/lsp.ts"; 2 | import { Params } from "../@ddc-sources/lsp.ts"; 3 | 4 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 5 | import * as fn from "jsr:@denops/std@~7.5.0/function"; 6 | import * as op from "jsr:@denops/std@~7.5.0/option"; 7 | 8 | // Copyright (c) 2019 hrsh7th 9 | // https://github.com/hrsh7th/vim-vsnip/blob/7753ba9c10429c29d25abfd11b4c60b76718c438/autoload/vsnip/indent.vim 10 | 11 | async function getOneIndent( 12 | denops: Denops, 13 | ): Promise { 14 | if (await op.expandtab.get(denops)) { 15 | let width = await op.shiftwidth.get(denops); 16 | if (width === 0) { 17 | width = await op.tabstop.get(denops); 18 | } 19 | return " ".repeat(width); 20 | } else { 21 | return "\t"; 22 | } 23 | } 24 | 25 | async function getBaseIndent( 26 | denops: Denops, 27 | ): Promise { 28 | const line = await fn.getline(denops, "."); 29 | return line.match(/^\s*/)?.[0] ?? ""; 30 | } 31 | 32 | async function adjustIndent( 33 | denops: Denops, 34 | text: string, 35 | ): Promise { 36 | const oneIndent = await getOneIndent(denops); 37 | const baseIndent = await getBaseIndent(denops); 38 | if (oneIndent !== "\t") { 39 | text = text.replaceAll( 40 | /(?<=^|\n)\t+/g, 41 | (match) => oneIndent.repeat(match.length), 42 | ); 43 | } 44 | // Add baseIndent to all lines except the first line. 45 | text = text.replaceAll(/\n/g, `\n${baseIndent}`); 46 | // Remove indentation on all blank lines except the last line. 47 | text = text.replaceAll(/\n\s*\n/g, "\n\n"); 48 | 49 | return text; 50 | } 51 | 52 | export async function expand( 53 | denops: Denops, 54 | body: string, 55 | snippetEngine: Params["snippetEngine"], 56 | ): Promise { 57 | if (snippetEngine !== "") { 58 | // snippetEngine is registered 59 | if (typeof snippetEngine === "string") { 60 | await denops.call( 61 | "denops#callback#call", 62 | snippetEngine, 63 | body, 64 | ); 65 | } else { 66 | await snippetEngine(body); 67 | } 68 | } else { 69 | const parsedText = parseSnippet(body); 70 | const adjustedText = await adjustIndent(denops, parsedText); 71 | await linePatch(denops, 0, 0, adjustedText); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/snippet_test.ts: -------------------------------------------------------------------------------- 1 | import { assertBuffer, searchCursor } from "./test_util.ts"; 2 | import { expand } from "./snippet.ts"; 3 | 4 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 5 | import * as nvim from "jsr:@denops/std@~7.5.0/function/nvim"; 6 | import * as op from "jsr:@denops/std@~7.5.0/option"; 7 | import { test } from "jsr:@denops/test@~3.0.2"; 8 | import { batch } from "jsr:@denops/std@~7.5.0/batch"; 9 | 10 | type Suite = { 11 | expandtab: boolean; 12 | shiftwidth: number; 13 | tabstop: number; 14 | buffer: string[]; 15 | body: string; 16 | expectBuffer: string[]; 17 | }; 18 | 19 | async function setup( 20 | denops: Denops, 21 | buffer: string[], 22 | ) { 23 | const { row, col } = searchCursor(buffer, ""); 24 | await nvim.nvim_buf_set_lines(denops, 0, 0, -1, true, buffer); 25 | await nvim.nvim_win_set_cursor(denops, 0, [row, col]); 26 | } 27 | 28 | test({ 29 | mode: "nvim", 30 | name: "snippet", 31 | fn: async (denops, t) => { 32 | const suites: Record = { 33 | expandtab: { 34 | expandtab: false, 35 | shiftwidth: 8, 36 | tabstop: 4, 37 | buffer: ["\t|foo"], 38 | body: "bar\n\tbaz\n", 39 | expectBuffer: [ 40 | "\tbar", 41 | "\t\tbaz", 42 | "\t|foo", 43 | ], 44 | }, 45 | shiftwidth: { 46 | expandtab: true, 47 | shiftwidth: 8, 48 | tabstop: 4, 49 | buffer: [" |foo"], 50 | body: "bar\n\tbaz\n", 51 | expectBuffer: [ 52 | " bar", 53 | " baz", 54 | " |foo", 55 | ], 56 | }, 57 | tabstop: { 58 | expandtab: true, 59 | shiftwidth: 0, 60 | tabstop: 4, 61 | buffer: [" |foo"], 62 | body: "bar\n\tbaz\n", 63 | expectBuffer: [ 64 | " bar", 65 | " baz", 66 | " |foo", 67 | ], 68 | }, 69 | blankline: { 70 | expandtab: false, 71 | shiftwidth: 4, 72 | tabstop: 4, 73 | buffer: ["\t|foo"], 74 | body: "bar\n\n\tbaz\n", 75 | expectBuffer: [ 76 | "\tbar", 77 | "", 78 | "\t\tbaz", 79 | "\t|foo", 80 | ], 81 | }, 82 | }; 83 | 84 | for (const [name, suite] of Object.entries(suites)) { 85 | await t.step({ 86 | name, 87 | fn: async () => { 88 | await batch(denops, async (denops) => { 89 | await op.expandtab.set(denops, suite.expandtab); 90 | await op.shiftwidth.set(denops, suite.shiftwidth); 91 | await op.tabstop.set(denops, suite.tabstop); 92 | }); 93 | await setup(denops, suite.buffer); 94 | await expand(denops, suite.body, ""); 95 | await assertBuffer(denops, suite.expectBuffer); 96 | }, 97 | }); 98 | } 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /denops/ddc-source-lsp/test_util.ts: -------------------------------------------------------------------------------- 1 | import { byteLength } from "./completion_item.ts"; 2 | 3 | import type { Denops } from "jsr:@denops/std@~7.5.0"; 4 | import * as nvim from "jsr:@denops/std@~7.5.0/function/nvim"; 5 | 6 | import { assertEquals } from "jsr:@std/assert@~1.0.0/equals"; 7 | 8 | // (1,0)-index, byte 9 | export function searchCursor( 10 | buffer: string[], 11 | insert: string, 12 | ): { row: number; col: number; completePos: number } { 13 | const line = buffer.findIndex((text) => text.includes("|")); 14 | if (line === -1) { 15 | throw new Error("Invalid buffer: cursor not found"); 16 | } 17 | const completePos = buffer[line].indexOf("|"); 18 | buffer[line] = buffer[line].replace("|", insert); 19 | const col = byteLength(buffer[line].slice(0, completePos) + insert); 20 | return { row: line + 1, col, completePos }; 21 | } 22 | 23 | export async function assertBuffer( 24 | denops: Denops, 25 | buffer: string[], 26 | ): Promise { 27 | const { row, col } = searchCursor(buffer, ""); 28 | 29 | const actualBuffer = await nvim.nvim_buf_get_lines(denops, 0, 0, -1, true); 30 | assertEquals(actualBuffer, buffer); 31 | const actualCursor = await nvim.nvim_win_get_cursor(denops, 0); 32 | assertEquals(actualCursor, [row, col]); 33 | } 34 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /doc/ddc-filter-converter_kind_labels.txt: -------------------------------------------------------------------------------- 1 | *ddc-filter-converter_kind_labels.txt* Change labels for each lsp kind 2 | 3 | Authors: Shougo 4 | uga-rosa 5 | License: MIT license 6 | 7 | CONTENTS *ddc-filter-converter_kind_labels-contents* 8 | 9 | Introduction |ddc-filter-converter_kind_labels-introduction| 10 | Examples |ddc-filter-converter_kind_labels-examples| 11 | Params |ddc-filter-converter_kind_labels-params| 12 | FAQ |ddc-filter-converter_kind_labels-faq| 13 | 14 | 15 | ============================================================================== 16 | INTRODUCTION *ddc-filter-converter_kind_labels-introduction* 17 | 18 | This filter converts lsp kind label to your setting. 19 | This converter is specifically disigned for "ddc-source-lsp". 20 | 21 | 22 | ============================================================================== 23 | EXAMPLES *ddc-filter-converter_kind_labels-examples* 24 | >vim 25 | call ddc#custom#patch_global(#{ 26 | \ sourceOptions: #{ 27 | \ lsp: #{ 28 | \ converters: [ 'converter_kind_labels' ], 29 | \ }, 30 | \ } 31 | \ }) 32 | < 33 | 34 | ============================================================================== 35 | PARAMS *ddc-filter-converter_kind_labels-params* 36 | 37 | *ddc-filter-converter_kind_labels-param-kindLabels* 38 | kindLabels (|Dictionary|) 39 | This represents your setting labels corresponding to lsp kind labels. 40 | Each your setting apply to each label. 41 | 42 | The valid keys are bellow. 43 | Text, 44 | Method, 45 | Function, 46 | Constructor, 47 | Field, 48 | Variable, 49 | Class, 50 | Interface, 51 | Module, 52 | Property, 53 | Unit, 54 | Value, 55 | Enum, 56 | Keyword, 57 | Snippet, 58 | Color, 59 | File, 60 | Reference, 61 | Folder, 62 | EnumMember, 63 | Constant, 64 | Struct, 65 | Event, 66 | Operator, 67 | TypeParameter 68 | 69 | Default: {} 70 | 71 | *ddc-filter-converter_kind_labels-param-kindHlGroups* 72 | kindHlGroups (|Dictionary|) 73 | This represents your setting labels highlight corresponding to lsp 74 | kind labels. 75 | Each your setting apply to each label. 76 | Dictionary value must be the highlight name such as "Identifier", 77 | "Red", "Function"... in |highlight-groups|. 78 | 79 | The valid keys are same above. 80 | 81 | Default: {} 82 | 83 | 84 | ============================================================================== 85 | FREQUENTLY ASKED QUESTIONS (FAQ) *ddc-filter-converter_kind_labels-faq* 86 | 87 | Q: I want to use "VSCode" like icon. 88 | 89 | A: You can setting like bellow. 90 | >vim 91 | call ddc#custom#patch_global(#{ 92 | \ filterParams: #{ 93 | \ converter_kind_labels: #{ 94 | \ kindLabels: #{ 95 | \ Text: '', 96 | \ Method: '', 97 | \ Function: '', 98 | \ Constructor: '', 99 | \ Field: '', 100 | \ Variable: '', 101 | \ Class: '', 102 | \ Interface: '', 103 | \ Module: '', 104 | \ Property: '', 105 | \ Unit: '', 106 | \ Value: '', 107 | \ Enum: '', 108 | \ Keyword: '', 109 | \ Snippet: '', 110 | \ Color: '', 111 | \ File: '', 112 | \ Reference: '', 113 | \ Folder: '', 114 | \ EnumMember: '', 115 | \ Constant: '', 116 | \ Struct: '', 117 | \ Event: '', 118 | \ Operator: '', 119 | \ TypeParameter: '', 120 | \ }, 121 | \ kindHlGroups: #{ 122 | \ Method: 'Function', 123 | \ Function: 'Function', 124 | \ Constructor: 'Function', 125 | \ Field: 'Identifier', 126 | \ Variable: 'Identifier', 127 | \ Class: 'Structure', 128 | \ Interface: 'Structure', 129 | \ } 130 | \ } 131 | \ } 132 | \ }) 133 | < 134 | 135 | ============================================================================== 136 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 137 | -------------------------------------------------------------------------------- /doc/ddc-filter-sorter_lsp-kind.txt: -------------------------------------------------------------------------------- 1 | *ddc-filter-sorter_lsp-kind.txt* Sort by LSP kinds. 2 | 3 | Author: uga-rosa 4 | License: MIT License 5 | 6 | CONTENTS *ddc-filter-sorter_lsp-kind-contents* 7 | 8 | Introduction |ddc-filter-sorter_lsp-kind-introduction| 9 | Examples |ddc-filter-sorter_lsp-kind-examples| 10 | Params |ddc-filter-sorter_lsp-kind-params| 11 | 12 | 13 | ============================================================================== 14 | INTRODUCTION *ddc-filter-sorter_lsp-kind-introduction* 15 | 16 | Sort source-lsp items by kinds. 17 | 18 | 19 | ============================================================================== 20 | EXAMPLES *ddc-filter-sorter_lsp-kind-examples* 21 | > 22 | call ddc#custom#patch_global(#{ 23 | \ sourceOptions: #{ 24 | \ lsp: #{ 25 | \ sorters: ['sorter_lsp-kind'] 26 | \ }, 27 | \ }, 28 | \ filterParams: #{ 29 | \ sorter_lsp-kind: #{ 30 | \ priority: [ 31 | \ 'Enum', 32 | \ ['Method', 'Function'], 33 | \ 'Field', 34 | \ 'Variable', 35 | \ ] 36 | \ } 37 | \ }, 38 | \}) 39 | < 40 | 41 | ============================================================================== 42 | PARAMS *ddc-filter-sorter_lsp-kind-params* 43 | 44 | *ddc-filter-sorter_lsp-kind-param-priority* 45 | priority (|List|) 46 | The type is `(LspKind | LspKind[])[]` and LspKind is a union type. 47 | > 48 | type LspKind = 49 | | "Text" 50 | | "Method" 51 | | "Function" 52 | | "Constructor" 53 | | "Field" 54 | | "Variable" 55 | | "Class" 56 | | "Interface" 57 | | "Module" 58 | | "Property" 59 | | "Unit" 60 | | "Value" 61 | | "Enum" 62 | | "Keyword" 63 | | "Snippet" 64 | | "Color" 65 | | "File" 66 | | "Reference" 67 | | "Folder" 68 | | "EnumMember" 69 | | "Constant" 70 | | "Struct" 71 | | "Event" 72 | | "Operator" 73 | | "TypeParameter" 74 | < 75 | The previous element has higher priority, and by making it an array, 76 | multiple LspKinds can have the same priority. For example, in 77 | |ddc-filter-sorter_lsp-kind-examples|, `Enum` has the highest 78 | priority, followed by `Method` and `Function` with the same priority, 79 | then `Field`, `Variable`, and so on. 80 | 81 | Default: [ 82 | "Snippet", 83 | "Method", 84 | "Function", 85 | "Constructor", 86 | "Field", 87 | "Variable", 88 | "Class", 89 | "Interface", 90 | "Module", 91 | "Property", 92 | "Unit", 93 | "Value", 94 | "Enum", 95 | "Keyword", 96 | "Color", 97 | "File", 98 | "Reference", 99 | "Folder", 100 | "EnumMember", 101 | "Constant", 102 | "Struct", 103 | "Event", 104 | "Operator", 105 | "TypeParameter", 106 | "Text", 107 | ] 108 | 109 | 110 | ============================================================================== 111 | vim:tw=78:ts=8:noet:ft=help:norl: 112 | -------------------------------------------------------------------------------- /doc/ddc-source-lsp.txt: -------------------------------------------------------------------------------- 1 | *ddc-source-lsp.txt* lsp completion for ddc.vim 2 | 3 | Authors: Shougo 4 | uga-rosa 5 | License: MIT license 6 | 7 | CONTENTS *ddc-source-lsp-contents* 8 | 9 | Introduction |ddc-source-lsp-introduction| 10 | Install |ddc-source-lsp-install| 11 | Examples |ddc-source-lsp-examples| 12 | Params |ddc-source-lsp-params| 13 | FAQ |ddc-source-lsp-faq| 14 | 15 | 16 | ============================================================================== 17 | INTRODUCTION *ddc-source-lsp-introduction* 18 | 19 | This source collects items from "nvim-lsp". 20 | 21 | 22 | ============================================================================== 23 | INSTALL *ddc-source-lsp-install* 24 | 25 | Please install both "ddc.vim" and "denops.vim". 26 | 27 | https://github.com/Shougo/ddc.vim 28 | https://github.com/vim-denops/denops.vim 29 | 30 | You must set up "nvim-lsp" (neovim builtin lsp client) or "vim-lsp" or 31 | "lspoints". 32 | https://github.com/prabirshrestha/vim-lsp 33 | https://github.com/kuuote/lspoints 34 | 35 | You also need a snippet plugin if you want to expand snippets in LSP format. 36 | e.g. 37 | https://github.com/hrsh7th/vim-vsnip 38 | https://github.com/SirVer/ultisnips 39 | https://github.com/L3MON4D3/LuaSnip 40 | https://github.com/dcampos/nvim-snippy 41 | If you are using neovim nightly, then |vim.snippet| would be another option. 42 | 43 | If you want to read the documentation for items, please use 44 | below plugins. 45 | 46 | https://github.com/Shougo/pum.vim 47 | https://github.com/matsui54/denops-popup-preview.vim 48 | https://github.com/uga-rosa/ddc-previewer-floating 49 | 50 | 51 | ============================================================================== 52 | EXAMPLES *ddc-source-lsp-examples* 53 | 54 | To take advantage of all the features, you need to set client_capabilities. 55 | 56 | >lua 57 | vim.lsp.config('*', { 58 | capabilities = require("ddc_source_lsp").make_client_capabilities(), 59 | }) 60 | < 61 | >vim 62 | call ddc#custom#patch_global('sources', ['lsp']) 63 | 64 | call ddc#custom#patch_global('sourceOptions', #{ 65 | \ lsp: #{ 66 | \ mark: 'lsp', 67 | \ forceCompletionPattern: '\.\w*|:\w*|->\w*', 68 | \ sorters: ['sorter_lsp-kind'], 69 | \ }, 70 | \ }) 71 | 72 | " Register snippet engine (vim-vsnip) 73 | call ddc#custom#patch_global('sourceParams', #{ 74 | \ lsp: #{ 75 | \ snippetEngine: denops#callback#register({ 76 | \ body -> vsnip#anonymous(body) 77 | \ }), 78 | \ } 79 | \ }) 80 | < 81 | 82 | ============================================================================== 83 | PARAMS *ddc-source-lsp-params* 84 | 85 | *ddc-source-lsp-param-bufnr* 86 | bufnr (number | v:null) 87 | - number: the number of a buffer 88 | - v:null: the current buffer 89 | 90 | Specify the buffer to be used in requests to the language 91 | server. Needs not be set for normal use. see FAQ 92 | (|ddc-source-lsp-faq-markdown-codeblocks|). 93 | 94 | Default: v:null 95 | 96 | *ddc-source-lsp-param-confirmBehavior* 97 | confirmBehavior ("insert" | "replace") 98 | - "insert": Inserts the selected item and moves adjacent 99 | text to the right. 100 | - "replace": Replaces adjacent text with the selected item. 101 | 102 | Default: "insert" 103 | 104 | *ddc-source-lsp-param-enableAdditionalTextEdit* 105 | enableAdditionalTextEdit (boolean) 106 | Enable supplementary editing apart from the cursor. For 107 | instance, it allows the auto-import of the 108 | typescript-language-server, as well as macro-expansion via 109 | rust-analyzer. 110 | NOTE: To use this feature, 111 | |ddc-source-lsp-param-enableResolveItem| must be 112 | |v:true|. 113 | 114 | Default: v:false 115 | 116 | *ddc-source-lsp-param-enableDisplayDetail* 117 | enableDisplayDetail (boolean) 118 | Set the detailed text in |ddc-item-attribute-menu| if 119 | possible. 120 | NOTE: The text depends on LSP server. 121 | NOTE: The text may be very long. 122 | 123 | Default: v:false 124 | 125 | *ddc-source-lsp-param-enableMatchLabel* 126 | enableMatchLabel (boolean) 127 | If it is enabled, item's |ddc-item-attribute-word| must be 128 | matched to LSP "label". 129 | NOTE: It can remove invalid items from LSP server. 130 | 131 | Default: v:false 132 | 133 | *ddc-source-lsp-param-enableResolveItem* 134 | enableResolveItem (boolean) 135 | Enable LSP's "completionItem/resolve" feature when the item is 136 | confirmed. 137 | NOTE: It must be enabled to enable LSP auto-import feature. 138 | 139 | Default: v:false 140 | 141 | *ddc-source-lsp-param-lspEngine* 142 | lspEngine (string) 143 | The LSP Engine to use. 144 | 145 | - "nvim-lsp": Use "nvim-lsp" engine. It is neovim builtin. 146 | 147 | - "vim-lsp": Use "vim-lsp" engine. 148 | https://github.com/prabirshrestha/vim-lsp 149 | 150 | - "lspoints": Use "lspoints" engine. It use "denops.vim". 151 | https://github.com/kuuote/lspoints 152 | 153 | Default: "nvim-lsp" 154 | 155 | *ddc-source-lsp-param-manualOnlyServers* 156 | manualOnlyServers (string[]) 157 | The LSP server names to use manual completion only. 158 | It is useful if the server is very slow performance. 159 | 160 | Default: [] 161 | 162 | *ddc-source-lsp-param-snippetEngine* 163 | snippetEngine (string | function) 164 | The language server may return snippet as items, so work with 165 | another plugin to expand it. Register with 166 | |denops#callback#register()| and specify its id in this param. 167 | 168 | NOTE: If you use |ddc#custom#load_config()|, you can pass 169 | TypeScript function directly. 170 | 171 | NOTE: If you do not use the snippet plugin, leave the empty 172 | string. Do not register functions that do nothing. 173 | >vim 174 | " https://github.com/hrsh7th/vim-vsnip 175 | denops#callback#register({ body -> vsnip#anonymous(body) }) 176 | " https://github.com/SirVer/ultisnips 177 | denops#callback#register({ body -> UltiSnips#Anon(body) }) 178 | < 179 | >lua 180 | -- https://github.com/L3MON4D3/LuaSnip 181 | vim.fn['denops#callback#register'](function(body) 182 | require('luasnip').lsp_expand(body) 183 | end) 184 | 185 | -- https://github.com/dcampos/nvim-snippy 186 | vim.fn['denops#callback#register'](function(body) 187 | require('snippy').expand_snippet(body) 188 | end) 189 | < 190 | Default: "" 191 | 192 | *ddc-source-lsp-param-snippetIndicator* 193 | snippetIndicator (string) 194 | The indicator string for snippet items added to the end of 195 | a completion item. 196 | 197 | NOTE: This affects ddc-item-attribute-abbr. 198 | 199 | Default: "~" 200 | 201 | ============================================================================== 202 | FREQUENTLY ASKED QUESTIONS (FAQ) *ddc-source-lsp-faq* 203 | 204 | *ddc-source-lsp-faq-snippet-is-doubled* 205 | Q: The snippet is double expanded. 206 | 207 | A: Remove "vim-vsnip-integ" and use "ddc-source-vsnip" instead. 208 | Since "vim-vsnip-integ" sets |:autocmd|, it conflicts with the source. Please 209 | remove it completely from 'runtimepath'. 210 | 211 | https://github.com/uga-rosa/ddc-source-vsnip 212 | 213 | *ddc-source-lsp-faq-items-not-displayed* 214 | Q: The items are not displayed. 215 | 216 | A: You have enabled LSP snippet by "snippetSupport" in lspconfig. 217 | If you want to complete snippet items, you must configure 218 | |ddc-source-lsp-param-snippetEngine|. Otherwise, they are filtered. 219 | 220 | *ddc-source-lsp-faq-markdown-codeblocks* 221 | Q: How to enable completions in markdown codeblocks? 222 | 223 | A: Use |ddc#custom#set_context_filetype()| and conditionally set the number of 224 | a virtual buffer to |ddc-source-lsp-param-bufnr|. The virual buffer should 225 | synchronize its content with the codeblock and should have an attached 226 | language server. In Neovim, otter.nvim (https://github.com/jmbuhr/otter.nvim) 227 | is the plugin to manage such buffers. Following is an example setup: 228 | 229 | >lua 230 | -- Activate otter on markdown 231 | vim.api.nvim_create_autocmd('Filetype', { 232 | pattern = 'lua', 233 | callback = function() 234 | require('otter').activate( 235 | {'lua'}, -- languages to enable completions 236 | false -- disable completion with cmp 237 | ) 238 | end 239 | }) 240 | 241 | vim.fn['ddc#custom#set_context_filetype']('markdown', function() 242 | local otter_keeper = require('otter.keeper') 243 | 244 | -- sync quarto buffer with otter buffers 245 | otter_keeper.sync_raft(ctx.buf) 246 | 247 | -- sourceParams based on the cursor positions 248 | local cursor = vim.api.nvim_win_get_cursor(0) 249 | local otter_attached = otter_keeper._otters_attached[ctx.buf] 250 | for _, chunks in pairs(otter_attached.code_chunks) do 251 | for _, chunk in pairs(chunks) do 252 | local srow, scol = chunk.range.from[1] + 1, chunk.range.from[2] 253 | local erow, ecol = chunk.range.to[1] + 1, chunk.range.to[2] 254 | if ((cursor[1] == srow and cursor[2] >= scol) 255 | or (cursor[1] > srow)) 256 | and ((cursor[1] == erow and cursor[2] <= ecol) 257 | or cursor[1] < erow) 258 | then 259 | return { 260 | sourceParams = { 261 | lsp = { bufnr = otter_attached.buffers[chunk.lang] }, 262 | }, 263 | } 264 | end 265 | end 266 | end 267 | 268 | -- if current cursor is not inside a codeblock, do nothing 269 | return {} 270 | end) 271 | < 272 | 273 | ============================================================================== 274 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 275 | -------------------------------------------------------------------------------- /lua/ddc_source_lsp/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param override? table 4 | ---@return table client_capabilities 5 | function M.make_client_capabilities(override) 6 | local capabilities = vim.lsp.protocol.make_client_capabilities() 7 | capabilities.textDocument.completion = { 8 | dynamicRegistration = false, 9 | completionItem = { 10 | snippetSupport = true, 11 | commitCharactersSupport = true, 12 | deprecatedSupport = true, 13 | preselectSupport = true, 14 | tagSupport = { 15 | valueSet = { 16 | 1, -- Deprecated 17 | }, 18 | }, 19 | insertReplaceSupport = true, 20 | resolveSupport = { 21 | properties = { 22 | "documentation", 23 | "detail", 24 | "additionalTextEdits", 25 | "insertText", 26 | "textEdit", 27 | "insertTextFormat", 28 | "insertTextMode", 29 | }, 30 | }, 31 | insertTextModeSupport = { 32 | valueSet = { 33 | 1, -- asIs 34 | 2, -- adjustIndentation 35 | }, 36 | }, 37 | labelDetailsSupport = true, 38 | }, 39 | contextSupport = true, 40 | insertTextMode = 1, 41 | completionList = { 42 | itemDefaults = { 43 | "commitCharacters", 44 | "editRange", 45 | "insertTextFormat", 46 | "insertTextMode", 47 | "data", 48 | }, 49 | }, 50 | } 51 | capabilities = vim.tbl_deep_extend("force", capabilities, override or {}) 52 | return capabilities 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /lua/ddc_source_lsp/internal.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@class Client 4 | ---@field id number 5 | ---@field name string 6 | ---@field provider table 7 | ---@field offsetEncoding string 8 | 9 | ---@param bufnr number? 10 | ---@return Client[] 11 | function M.get_clients(bufnr) 12 | local clients = {} 13 | ---@diagnostic disable-next-line: deprecated 14 | local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients 15 | for _, client in pairs(get_clients({ bufnr = bufnr or 0 })) do 16 | local provider = client.server_capabilities.completionProvider 17 | if provider then 18 | table.insert(clients, { 19 | id = client.id, 20 | name = client.name, 21 | provider = provider, 22 | offsetEncoding = client.offset_encoding, 23 | }) 24 | end 25 | end 26 | return clients 27 | end 28 | 29 | ---Neovim may put true, etc. in key when converting from vim script. 30 | ---:h lua-special-tbl 31 | ---@param tbl table 32 | ---@return table 33 | local function normalize(tbl) 34 | for key, value in pairs(tbl) do 35 | local key_t = type(key) 36 | if key_t == "string" or key_t == "number" then 37 | if type(value) == "table" then 38 | normalize(value) 39 | end 40 | else 41 | tbl[key] = nil 42 | end 43 | end 44 | return tbl 45 | end 46 | 47 | ---Doesn't block Nvim, but cannot be used in denops#request() 48 | ---@param clientId number 49 | ---@param method string 50 | ---@param params table 51 | ---@param opts { plugin_name: string, lambda_id: string, bufnr: number? } 52 | ---@return unknown? 53 | function M.request(clientId, method, params, opts) 54 | local client = vim.lsp.get_client_by_id(clientId) 55 | if client then 56 | client:request(method, normalize(params), function(err, result) 57 | if err == nil and result then 58 | vim.fn["denops#notify"](opts.plugin_name, opts.lambda_id, { result }) 59 | end 60 | end, opts.bufnr or 0) 61 | end 62 | end 63 | 64 | ---Blocks Nvim, but can be used in denops#request() 65 | ---@param clientId number 66 | ---@param method string 67 | ---@param params table 68 | ---@param opts { timeout: number, bufnr: number? } 69 | ---@return unknown? 70 | function M.request_sync(clientId, method, params, opts) 71 | local client = vim.lsp.get_client_by_id(clientId) 72 | if client then 73 | local resp = client:request_sync(method, normalize(params), opts.timeout, opts.bufnr or 0) 74 | if resp and resp.err == nil and resp.result then 75 | return resp.result 76 | end 77 | end 78 | end 79 | 80 | ---@param clientId number 81 | ---@param command lsp.Command 82 | function M.execute(clientId, command) 83 | local client = vim.lsp.get_client_by_id(clientId) 84 | if client == nil or not client.server_capabilities.executeCommandProvider then 85 | return 86 | end 87 | command.title = nil 88 | client:request("workspace/executeCommand", command, nil, 0) 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /plugin/ddc_source_lsp.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_ddc_source_lsp') 2 | finish 3 | endif 4 | let g:loaded_ddc_source_lsp = 1 5 | 6 | function s:set_default_highlight() abort 7 | highlight default DdcLspDeprecated 8 | \ term=strikethrough cterm=strikethrough gui=strikethrough 9 | endfunction 10 | 11 | call s:set_default_highlight() 12 | 13 | " Cleared default highlights without link when applying colorscheme 14 | " so redefine it. 15 | augroup ddc-source-lsp 16 | autocmd! 17 | autocmd ColorScheme * call set_default_highlight() 18 | augroup END 19 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | --------------------------------------------------------------------------------