├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── jsr.yml └── FUNDING.yml ├── README.md ├── denops └── @ddu-uis │ └── ff │ ├── deno.json │ ├── preview.ts │ └── main.ts ├── deno.jsonc ├── LICENSE ├── autoload └── ddu │ └── ui │ └── ff.vim └── doc └── ddu-ui-ff.txt /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddu-ui-ff 2 | 3 | Fuzzy finder UI for ddu.vim 4 | 5 | This UI is standard fuzzy finder. 6 | 7 | ## Required 8 | 9 | ### denops.vim 10 | 11 | https://github.com/vim-denops/denops.vim 12 | 13 | ### ddu.vim 14 | 15 | https://github.com/Shougo/ddu.vim 16 | 17 | ## Configuration 18 | 19 | ```vim 20 | call ddu#custom#patch_global(#{ 21 | \ ui: 'ff', 22 | \ }) 23 | ``` 24 | -------------------------------------------------------------------------------- /denops/@ddu-uis/ff/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shougo/ddu-ui-ff", 3 | "exports": { 4 | ".": "./main.ts" 5 | }, 6 | "publish": { 7 | "include": [ 8 | "**/*.ts" 9 | ] 10 | }, 11 | "imports": { 12 | "@core/unknownutil": "jsr:@core/unknownutil@~4.3.0", 13 | "@denops/std": "jsr:@denops/std@~8.0.0", 14 | "@shougo/ddu-vim": "jsr:@shougo/ddu-vim@~11.0.0", 15 | "@std/assert": "jsr:@std/assert@~1.0.7", 16 | "@std/path": "jsr:@std/path@~1.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/jsr.yml: -------------------------------------------------------------------------------- 1 | name: jsr 2 | 3 | env: 4 | DENO_VERSION: 2.x 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: denoland/setup-deno@v2 23 | with: 24 | deno-version: ${{ env.DENO_VERSION }} 25 | - name: Publish 26 | run: | 27 | deno run -A jsr:@david/publish-on-tag@0.2.0 28 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "cache": "deno install --reload", 5 | "check": "deno check denops/**/*.ts", 6 | "lint": "deno lint denops", 7 | "lint-fix": "deno lint --fix denops", 8 | "fmt": "deno fmt denops", 9 | "test": "deno test -A --doc --parallel --shuffle denops/**/*.ts", 10 | "test:publish": "deno publish --dry-run --allow-dirty --set-version 0.0.0", 11 | "update": "deno outdated --recursive", 12 | "upgrade": "deno outdated --recursive --update" 13 | }, 14 | "workspace": [ 15 | "./denops/@ddu-uis/ff" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Shougo 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** A clear and 10 | concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** A clear and concise description of what you 13 | want to happen. 14 | 15 | **Describe alternatives you've considered** A clear and concise description of 16 | any alternative solutions or features you've considered. 17 | 18 | **Additional context** Add any other context or screenshots about the feature 19 | request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Warning: I will close the issue without the minimal init.vim and the 10 | reproduction instructions.** 11 | 12 | # Problems summary 13 | 14 | ## Expected 15 | 16 | ## Environment Information 17 | 18 | - ddu.vim version (SHA1): 19 | 20 | - denops.vim version (SHA1): 21 | 22 | - deno version(`deno -V` output): 23 | 24 | - OS: 25 | 26 | - Neovim/Vim `:version` output: 27 | 28 | ## Provide a minimal init.vim/vimrc without plugin managers (Required!) 29 | 30 | ```vim 31 | " Your minimal init.vim/vimrc 32 | set runtimepath+=~/path/to/ddu.vim/ 33 | ``` 34 | 35 | ## How to reproduce the problem from Neovim/Vim startup (Required!) 36 | 37 | 1. foo 38 | 2. bar 39 | 3. baz 40 | 41 | ## Screenshot (if possible) 42 | 43 | ## Upload the log messages by `:redir` and `:message` (if errored) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) Shougo Matsushita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /denops/@ddu-uis/ff/preview.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionFlags, 3 | type BaseParams, 4 | type BufferPreviewer, 5 | type Context, 6 | type DduItem, 7 | type NoFilePreviewer, 8 | type PreviewContext, 9 | type Previewer, 10 | type TerminalPreviewer, 11 | } from "@shougo/ddu-vim/types"; 12 | import { printError } from "@shougo/ddu-vim/utils"; 13 | 14 | import type { Denops } from "@denops/std"; 15 | import { batch } from "@denops/std/batch"; 16 | import { replace } from "@denops/std/buffer"; 17 | import * as op from "@denops/std/option"; 18 | import * as fn from "@denops/std/function"; 19 | 20 | import { equal } from "@std/assert/equal"; 21 | import { ensure } from "@core/unknownutil/ensure"; 22 | import { is } from "@core/unknownutil/is"; 23 | 24 | import type { Params } from "./main.ts"; 25 | 26 | type PreviewParams = { 27 | syntaxLimitChars?: number; 28 | }; 29 | 30 | export class PreviewUi { 31 | #previewWinId = -1; 32 | #previewedTarget?: DduItem; 33 | #previewedUiParams?: Params; 34 | #matchIds: Record = {}; 35 | #previewedBufnrs: Set = new Set(); 36 | 37 | async close(denops: Denops, context: Context, uiParams: Params) { 38 | await this.clearHighlight(denops); 39 | 40 | if (!this.visible()) { 41 | return; 42 | } 43 | 44 | if (uiParams.previewFloating && denops.meta.host !== "nvim") { 45 | await denops.call("popup_close", this.#previewWinId); 46 | } else { 47 | const saveId = await fn.win_getid(denops); 48 | await batch(denops, async (denops) => { 49 | await fn.win_gotoid(denops, this.#previewWinId); 50 | if (this.#previewWinId === context.winId) { 51 | await denops.cmd( 52 | context.bufName === "" ? "enew" : `buffer ${context.bufNr}`, 53 | ); 54 | } else { 55 | await denops.cmd("close!"); 56 | } 57 | await fn.win_gotoid(denops, saveId); 58 | }); 59 | } 60 | this.#previewWinId = -1; 61 | } 62 | 63 | async removePreviewedBuffers(denops: Denops) { 64 | await batch(denops, async (denops) => { 65 | for (const bufnr of this.#previewedBufnrs) { 66 | await denops.cmd( 67 | `if bufexists(${bufnr}) && bufwinnr(${bufnr}) < 0 | silent bwipeout! ${bufnr} | endif`, 68 | ); 69 | } 70 | }); 71 | } 72 | 73 | async execute( 74 | denops: Denops, 75 | command: string, 76 | ) { 77 | if (!this.visible()) { 78 | return; 79 | } 80 | await fn.win_execute(denops, this.#previewWinId, command); 81 | } 82 | 83 | get previewWinId(): number { 84 | return this.#previewWinId; 85 | } 86 | 87 | isAlreadyPreviewed(item: DduItem): boolean { 88 | return this.visible() && equal(item, this.#previewedTarget); 89 | } 90 | 91 | isChangedUiParams(params: Params): boolean { 92 | return equal(params, this.#previewedUiParams); 93 | } 94 | 95 | visible(): boolean { 96 | return this.#previewWinId > 0; 97 | } 98 | 99 | async previewContents( 100 | denops: Denops, 101 | context: Context, 102 | uiParams: Params, 103 | actionParams: BaseParams, 104 | bufnr: number, 105 | item: DduItem, 106 | getPreviewer?: ( 107 | denops: Denops, 108 | item: DduItem, 109 | actionParams: BaseParams, 110 | previewContext: PreviewContext, 111 | ) => Promise, 112 | ): Promise { 113 | if (this.isAlreadyPreviewed(item) || !getPreviewer) { 114 | return ActionFlags.None; 115 | } 116 | 117 | const fileSize = item.status?.size ?? -1; 118 | if (fileSize > uiParams.previewMaxSize) { 119 | await printError( 120 | denops, 121 | `[ddu-ui-filer] The file size ${fileSize} is than previewMaxSize.`, 122 | ); 123 | return ActionFlags.None; 124 | } 125 | 126 | const prevId = await fn.win_getid(denops); 127 | const previewParams = ensure(actionParams, is.Record) as PreviewParams; 128 | 129 | const previewContext: PreviewContext = { 130 | col: Number(uiParams.previewCol), 131 | row: Number(uiParams.previewRow), 132 | width: Number(uiParams.previewWidth), 133 | height: Number(uiParams.previewHeight), 134 | isFloating: uiParams.previewFloating, 135 | split: uiParams.previewSplit, 136 | }; 137 | const previewer = await getPreviewer( 138 | denops, 139 | item, 140 | actionParams, 141 | previewContext, 142 | ); 143 | if (!previewer) { 144 | return ActionFlags.None; 145 | } 146 | 147 | let flag: ActionFlags; 148 | // Render the preview 149 | if (previewer.kind === "terminal") { 150 | flag = await this.#previewContentsTerminal( 151 | denops, 152 | previewer, 153 | uiParams, 154 | bufnr, 155 | context.winId, 156 | ); 157 | } else { 158 | flag = await this.#previewContentsBuffer( 159 | denops, 160 | previewer, 161 | uiParams, 162 | previewParams, 163 | bufnr, 164 | context.winId, 165 | item, 166 | ); 167 | } 168 | if (flag === ActionFlags.None) { 169 | return flag; 170 | } 171 | 172 | if (uiParams.previewFloating && denops.meta.host === "nvim") { 173 | const highlight = uiParams.highlights?.floating ?? "NormalFloat"; 174 | const borderHighlight = uiParams.highlights?.floatingBorder ?? 175 | "FloatBorder"; 176 | const cursorLineHighlight = uiParams.highlights?.floatingCursorLine ?? 177 | "CursorLine"; 178 | await fn.setwinvar( 179 | denops, 180 | this.#previewWinId, 181 | "&winhighlight", 182 | `Normal:${highlight},FloatBorder:${borderHighlight},CursorLine:${cursorLineHighlight}`, 183 | ); 184 | } 185 | 186 | await this.#jump(denops, previewer); 187 | 188 | if (uiParams.onPreview) { 189 | if (typeof uiParams.onPreview === "string") { 190 | await denops.call( 191 | "denops#callback#call", 192 | uiParams.onPreview, 193 | { 194 | context, 195 | item, 196 | previewContext, 197 | previewWinId: this.#previewWinId, 198 | }, 199 | ); 200 | } else { 201 | await uiParams.onPreview({ 202 | denops, 203 | context, 204 | item, 205 | previewContext, 206 | previewWinId: this.#previewWinId, 207 | }); 208 | } 209 | } 210 | 211 | this.#previewedBufnrs.add(await fn.bufnr(denops)); 212 | this.#previewedTarget = item; 213 | this.#previewedUiParams = uiParams; 214 | await fn.win_gotoid(denops, prevId); 215 | 216 | return ActionFlags.Persist; 217 | } 218 | 219 | async #previewContentsTerminal( 220 | denops: Denops, 221 | previewer: TerminalPreviewer, 222 | uiParams: Params, 223 | bufnr: number, 224 | previousWinId: number, 225 | ): Promise { 226 | if (!this.visible()) { 227 | this.#previewWinId = await denops.call( 228 | "ddu#ui#ff#_open_preview_window", 229 | uiParams, 230 | bufnr, 231 | bufnr, 232 | previousWinId, 233 | this.#previewWinId, 234 | ) as number; 235 | } else { 236 | await fn.win_gotoid(denops, this.#previewWinId); 237 | } 238 | 239 | // NOTE: ":terminal" overwrites current buffer. 240 | // NOTE: Use enew! to ignore E948 241 | await denops.cmd("enew!"); 242 | 243 | const opts: Record = {}; 244 | if (previewer.cwd) { 245 | opts.cwd = previewer.cwd; 246 | } 247 | 248 | if (denops.meta.host === "nvim") { 249 | if (await fn.has(denops, "nvim-0.11")) { 250 | // NOTE: termopen() is deprecated. 251 | await denops.call("jobstart", previewer.cmds, { 252 | ...opts, 253 | term: true, 254 | }); 255 | } else { 256 | await denops.call("termopen", previewer.cmds, opts); 257 | } 258 | } else { 259 | await denops.call("term_start", previewer.cmds, { 260 | ...opts, 261 | curwin: true, 262 | term_kill: "kill", 263 | }); 264 | } 265 | 266 | return ActionFlags.Persist; 267 | } 268 | 269 | async #previewContentsBuffer( 270 | denops: Denops, 271 | previewer: BufferPreviewer | NoFilePreviewer, 272 | uiParams: Params, 273 | actionParams: PreviewParams, 274 | bufnr: number, 275 | previousWinId: number, 276 | item: DduItem, 277 | ): Promise { 278 | if ( 279 | previewer.kind === "nofile" && !previewer.contents?.length || 280 | previewer.kind === "buffer" && !previewer.expr && !previewer.path 281 | ) { 282 | return ActionFlags.None; 283 | } 284 | 285 | const buffer = await this.#getPreviewBuffer(denops, previewer, item); 286 | const exists = await fn.bufexists(denops, buffer.bufnr); 287 | let previewBufnr = buffer.bufnr; 288 | const [err, contents] = await this.#getContents(denops, previewer); 289 | if (err || !exists || previewer.kind === "nofile") { 290 | // Create new buffer 291 | previewBufnr = await fn.bufadd(denops, buffer.bufname); 292 | await batch(denops, async (denops: Denops) => { 293 | await fn.setbufvar(denops, previewBufnr, "&buftype", "nofile"); 294 | await fn.setbufvar(denops, previewBufnr, "&swapfile", 0); 295 | await fn.setbufvar(denops, previewBufnr, "&bufhidden", "hide"); 296 | await fn.setbufvar(denops, previewBufnr, "&modeline", 1); 297 | 298 | await fn.bufload(denops, previewBufnr); 299 | await replace(denops, previewBufnr, contents); 300 | }); 301 | } 302 | 303 | this.#previewWinId = await denops.call( 304 | "ddu#ui#ff#_open_preview_window", 305 | uiParams, 306 | bufnr, 307 | previewBufnr, 308 | previousWinId, 309 | this.#previewWinId, 310 | ) as number; 311 | 312 | const limit = actionParams.syntaxLimitChars ?? 400000; 313 | if (!err && contents.join("\n").length < limit) { 314 | if (previewer.filetype) { 315 | await fn.setbufvar( 316 | denops, 317 | previewBufnr, 318 | "&filetype", 319 | previewer.filetype, 320 | ); 321 | } 322 | 323 | if (previewer.syntax) { 324 | await fn.setbufvar( 325 | denops, 326 | previewBufnr, 327 | "&syntax", 328 | previewer.syntax, 329 | ); 330 | } 331 | 332 | const filetype = await fn.getbufvar( 333 | denops, 334 | previewBufnr, 335 | "&filetype", 336 | ) as string; 337 | const syntax = await fn.getbufvar( 338 | denops, 339 | previewBufnr, 340 | "&syntax", 341 | ) as string; 342 | if (filetype.length === 0 && syntax.length === 0) { 343 | // NOTE: Call filetype detection by "BufRead" autocmd. 344 | // "filetype detect" is broken for the window. 345 | await fn.win_execute( 346 | denops, 347 | this.#previewWinId, 348 | "silent! doautocmd BufRead", 349 | ); 350 | } 351 | } 352 | 353 | // Set options 354 | await batch(denops, async (denops: Denops) => { 355 | for (const [option, value] of uiParams.previewWindowOptions) { 356 | await fn.setwinvar(denops, this.#previewWinId, option, value); 357 | } 358 | }); 359 | 360 | if (!err) { 361 | await this.#highlight( 362 | denops, 363 | previewer, 364 | previewBufnr, 365 | uiParams.highlights?.preview ?? "Search", 366 | ); 367 | } 368 | 369 | return ActionFlags.Persist; 370 | } 371 | 372 | async #getPreviewBuffer( 373 | denops: Denops, 374 | previewer: BufferPreviewer | NoFilePreviewer, 375 | item: DduItem, 376 | ): Promise<{ 377 | bufname: string; 378 | bufnr: number; 379 | }> { 380 | let bufname = ""; 381 | if (previewer.kind === "buffer") { 382 | if (previewer.expr) { 383 | const name = await fn.bufname(denops, previewer.expr); 384 | if (previewer.useExisting) { 385 | if (typeof previewer.expr === "string") { 386 | return { 387 | bufname: previewer.expr, 388 | bufnr: await fn.bufnr(denops, previewer.expr), 389 | }; 390 | } else { 391 | return { 392 | bufname: name, 393 | bufnr: previewer.expr, 394 | }; 395 | } 396 | } else if (!name.length) { 397 | bufname = `ddu-ff:no-name:${previewer.expr}`; 398 | } else { 399 | bufname = `ddu-ff:${name}`; 400 | } 401 | } else { 402 | bufname = `ddu-ff:${previewer.path}`; 403 | } 404 | } else if (previewer.kind === "nofile") { 405 | bufname = `ddu-ff:preview`; 406 | } else { 407 | bufname = `ddu-ff:${item.word}`; 408 | } 409 | 410 | return { 411 | bufname, 412 | bufnr: await fn.bufnr(denops, bufname), 413 | }; 414 | } 415 | 416 | async #getContents( 417 | denops: Denops, 418 | previewer: BufferPreviewer | NoFilePreviewer, 419 | ): Promise<[err: true | undefined, contents: string[]]> { 420 | if (previewer.kind !== "buffer") { 421 | return [undefined, previewer.contents]; 422 | } 423 | 424 | try { 425 | const bufferPath = previewer.expr ?? previewer.path; 426 | const stat = await safeStat(previewer.path); 427 | if (previewer.path && stat && !stat.isDirectory) { 428 | const data = Deno.readFileSync(previewer.path); 429 | const contents = new TextDecoder().decode(data).split("\n"); 430 | return [undefined, contents]; 431 | } else if (bufferPath && await fn.bufexists(denops, bufferPath)) { 432 | // Use buffer instead. 433 | const bufnr = await fn.bufnr(denops, bufferPath); 434 | await fn.bufload(denops, bufnr); 435 | const contents = await fn.getbufline(denops, bufnr, 1, "$"); 436 | return [undefined, contents]; 437 | } else { 438 | throw new Error(`"${previewer.path}" cannot be opened.`); 439 | } 440 | } catch (e: unknown) { 441 | const contents = [ 442 | "Error", 443 | `${(e as Error)?.message ?? e}`, 444 | ]; 445 | return [true, contents]; 446 | } 447 | } 448 | 449 | async #jump(denops: Denops, previewer: Previewer) { 450 | const pattern = "pattern" in previewer && previewer.pattern 451 | ? previewer.pattern 452 | : ""; 453 | const lineNr = "lineNr" in previewer && previewer.lineNr 454 | ? previewer.lineNr 455 | : 0; 456 | await denops.call("ddu#ui#ff#_jump", this.#previewWinId, pattern, lineNr); 457 | } 458 | 459 | async #highlight( 460 | denops: Denops, 461 | previewer: BufferPreviewer | NoFilePreviewer, 462 | bufnr: number, 463 | hlName: string, 464 | ) { 465 | // Clear the previous highlight 466 | await this.clearHighlight(denops); 467 | 468 | const ns = denops.meta.host === "nvim" 469 | ? await denops.call("nvim_create_namespace", "ddu-ui-ff-preview") 470 | : 0; 471 | 472 | const winid = this.#previewWinId; 473 | const maxRow = await denops.call("ddu#ui#ff#_max_row", bufnr); 474 | 475 | if (previewer.lineNr) { 476 | await denops.call( 477 | "ddu#ui#ff#_highlight", 478 | hlName, 479 | "lineNr", 480 | 1, 481 | ns, 482 | bufnr, 483 | previewer.lineNr, 484 | maxRow, 485 | 1, 486 | await denops.call("ddu#ui#ff#_max_col", bufnr, previewer.lineNr), 487 | await op.columns.get(denops), 488 | ); 489 | } else if (previewer.pattern) { 490 | this.#matchIds[winid] = await fn.matchadd( 491 | denops, 492 | hlName, 493 | previewer.pattern, 494 | 1, 495 | -1, 496 | { 497 | window: winid, 498 | }, 499 | ) as number; 500 | } 501 | 502 | await batch(denops, async (denops) => { 503 | if (!previewer.highlights) { 504 | return; 505 | } 506 | 507 | for (const hl of previewer.highlights) { 508 | await denops.call( 509 | "ddu#ui#ff#_highlight", 510 | hl.hl_group, 511 | hl.name, 512 | 1, 513 | ns, 514 | bufnr, 515 | hl.row, 516 | maxRow, 517 | hl.col, 518 | await denops.call("ddu#ui#ff#_max_col", bufnr, hl.row), 519 | hl.width, 520 | ); 521 | } 522 | }); 523 | } 524 | 525 | async clearHighlight(denops: Denops) { 526 | if (!this.visible()) { 527 | return; 528 | } 529 | const winid = this.#previewWinId; 530 | 531 | if (this.#matchIds[winid] > 0 && await fn.winbufnr(denops, winid) > 0) { 532 | try { 533 | await fn.matchdelete(denops, this.#matchIds[winid], winid); 534 | } catch (_: unknown) { 535 | // Ignore error 536 | } 537 | this.#matchIds[winid] = 0; 538 | } 539 | 540 | if (denops.meta.host === "nvim") { 541 | const ns = await denops.call( 542 | "nvim_create_namespace", 543 | "ddu-ui-ff-preview", 544 | ); 545 | await denops.call("nvim_buf_clear_namespace", 0, ns, 0, -1); 546 | } else { 547 | await denops.call( 548 | "prop_clear", 549 | 1, 550 | await denops.call("line", "$", winid), 551 | { 552 | bufnr: await fn.winbufnr(denops, winid), 553 | }, 554 | ); 555 | } 556 | } 557 | } 558 | 559 | const safeStat = async ( 560 | path: string | undefined, 561 | ): Promise => { 562 | if (!path) { 563 | return null; 564 | } 565 | 566 | // NOTE: Deno.stat() may be failed 567 | try { 568 | const stat = await Deno.stat(path); 569 | return stat; 570 | } catch (_: unknown) { 571 | // Ignore stat exception 572 | } 573 | return null; 574 | }; 575 | -------------------------------------------------------------------------------- /autoload/ddu/ui/ff.vim: -------------------------------------------------------------------------------- 1 | let s:namespace = has('nvim') ? nvim_create_namespace('ddu-ui-ff') : 0 2 | 3 | function ddu#ui#ff#_update_buffer( 4 | \ params, bufnr, winid, lines, refreshed, pos) abort 5 | const current_lines = '$'->line(a:winid) 6 | 7 | call setbufvar(a:bufnr, '&modifiable', v:true) 8 | 9 | try 10 | " NOTE: deletebufline() changes cursor position. 11 | " NOTE: deletebufline() needs ":silent". 12 | const before_cursor = a:winid->getcurpos() 13 | if a:lines->empty() 14 | " Clear buffer 15 | if current_lines > 1 16 | silent call deletebufline(a:bufnr, 1, '$') 17 | else 18 | call setbufline(a:bufnr, 1, ['']) 19 | endif 20 | else 21 | const footer_width = a:params.maxWidth / 3 22 | const lines = a:lines->map({ _, val -> 23 | \ ddu#ui#ff#_truncate( 24 | \ val, a:params.maxWidth, footer_width, '..') 25 | \ }) 26 | call setbufline(a:bufnr, 1, 27 | \ a:params.reversed ? reverse(lines) : lines) 28 | 29 | if current_lines > lines->len() 30 | silent call deletebufline(a:bufnr, lines->len() + 1, '$') 31 | endif 32 | endif 33 | catch 34 | " NOTE: Buffer modify may be failed 35 | call ddu#util#print_error(v:exception) 36 | return 37 | finally 38 | call setbufvar(a:bufnr, '&modifiable', v:false) 39 | call setbufvar(a:bufnr, '&modified', v:false) 40 | endtry 41 | 42 | if !a:refreshed && a:winid->getcurpos() ==# before_cursor 43 | return 44 | endif 45 | 46 | " Init the cursor 47 | const lnum = 48 | \ a:pos <= 0 49 | \ ? before_cursor[1] 50 | \ : a:params.reversed 51 | \ ? a:lines->len() - a:pos 52 | \ : a:pos 53 | const win_height = a:winid->winheight() 54 | const max_line = '$'->line(a:winid) 55 | if max_line - lnum < win_height / 2 56 | " Adjust cursor position when cursor is near bottom. 57 | call win_execute(a:winid, 'normal! Gzb') 58 | endif 59 | call win_execute(a:winid, 'call cursor(' .. lnum .. ', 0)') 60 | endfunction 61 | 62 | function ddu#ui#ff#_process_items( 63 | \ params, bufnr, max_lines, items, selected_items) abort 64 | " Buffer must be loaded 65 | if !a:bufnr->bufloaded() 66 | return 67 | endif 68 | 69 | " Clear all properties 70 | if has('nvim') 71 | call nvim_buf_clear_namespace(0, s:namespace, 0, -1) 72 | else 73 | call prop_clear(1, a:max_lines + 1, #{ bufnr: a:bufnr }) 74 | for prop_type in prop_type_list(#{ bufnr: a:bufnr }) 75 | call prop_type_delete(prop_type, #{ bufnr: a:bufnr }) 76 | endfor 77 | endif 78 | 79 | const max_row = ddu#ui#ff#_max_row(a:bufnr) 80 | 81 | for item in a:items 82 | call s:add_info_texts(a:bufnr, item.info, item.row) 83 | 84 | let row = a:params.reversed ? a:max_lines - item.row + 1 : item.row 85 | let max_col = ddu#ui#ff#_max_col(a:bufnr, row) 86 | 87 | " Highlights items 88 | for hl in item.highlights 89 | call ddu#ui#ff#_highlight( 90 | \ hl.hl_group, hl.name, 1, 91 | \ s:namespace, a:bufnr, 92 | \ row, 93 | \ max_row, 94 | \ hl.col + item.prefix->strlen(), 95 | \ max_col, 96 | \ hl.width 97 | \ ) 98 | endfor 99 | endfor 100 | 101 | " Selected items highlights 102 | let selected_highlight = a:params.highlights->get('selected', 'Statement') 103 | for item_nr in a:selected_items 104 | let row = a:params.reversed ? a:max_lines - item_nr : item_nr + 1 105 | let max_col = ddu#ui#ff#_max_col(a:bufnr, row) 106 | 107 | call ddu#ui#ff#_highlight( 108 | \ selected_highlight, 'ddu-ui-selected', 10000, 109 | \ s:namespace, a:bufnr, 110 | \ row, 111 | \ max_row, 112 | \ 1, 113 | \ max_col, 114 | \ 0 115 | \ ) 116 | endfor 117 | 118 | " NOTE: :redraw is needed 119 | redraw 120 | endfunction 121 | 122 | function s:add_info_texts(bufnr, info, row) abort 123 | if a:row <= 0 || a:row > line('$') 124 | " Invalid range 125 | return 126 | endif 127 | 128 | if has('nvim') 129 | call nvim_buf_set_extmark(0, s:namespace, a:row - 1, 0, #{ 130 | \ virt_lines: a:info->mapnew({ _, val -> 131 | \ val->has_key('hl_group') 132 | \ ? [[val.text, val.hl_group]] 133 | \ : [[val.text]] 134 | \ }), 135 | \ }) 136 | else 137 | for index in a:info->len()->range() 138 | let info = a:info[index] 139 | let prop_type = 'ddu-ui-info-' .. a:row .. '-' .. index 140 | 141 | let prop_type_opts = #{ 142 | \ bufnr: a:bufnr, 143 | \ priority: 10000, 144 | \ override: v:true, 145 | \ } 146 | if info->has_key('hl_group') 147 | let prop_type_opts.highlight = info.hl_group 148 | endif 149 | call prop_type_add(prop_type, prop_type_opts) 150 | 151 | call prop_add(a:row, 0, #{ 152 | \ type: prop_type, 153 | \ text: info.text, 154 | \ text_align: 'below', 155 | \ }) 156 | endfor 157 | endif 158 | endfunction 159 | 160 | function ddu#ui#ff#_max_row(bufnr) 161 | return a:bufnr->getbufinfo() 162 | \ ->get(0, #{ linecount: 0 })->get('linecount', 0) 163 | endfunction 164 | 165 | function ddu#ui#ff#_max_col(bufnr, row) 166 | return a:bufnr->getbufoneline(a:row)->len() 167 | endfunction 168 | 169 | function ddu#ui#ff#_highlight( 170 | \ highlight, prop_type, priority, id, bufnr, 171 | \ row, max_row, col, max_col, length) abort 172 | 173 | if !a:highlight->hlexists() 174 | call ddu#util#print_error( 175 | \ printf('highlight "%s" does not exist', a:highlight)) 176 | return 177 | endif 178 | 179 | if a:row <= 0 || a:col <= 0 || a:row > a:max_row || a:col > a:max_col 180 | " Invalid range 181 | return 182 | endif 183 | 184 | const length = 185 | \ a:length <= 0 || a:col + a:length > a:max_col 186 | \ ? a:max_col - a:col + 1 187 | \ : a:length 188 | 189 | if !has('nvim') 190 | " Add prop_type 191 | if a:prop_type->prop_type_get(#{ bufnr: a:bufnr })->empty() 192 | call prop_type_add(a:prop_type, #{ 193 | \ bufnr: a:bufnr, 194 | \ highlight: a:highlight, 195 | \ priority: a:priority, 196 | \ override: v:true, 197 | \ }) 198 | endif 199 | endif 200 | 201 | if has('nvim') 202 | call nvim_buf_set_extmark( 203 | \ a:bufnr, 204 | \ a:id, 205 | \ a:row - 1, 206 | \ a:col - 1, 207 | \ #{ 208 | \ end_col: a:col - 1 + length, 209 | \ hl_group: a:highlight, 210 | \ } 211 | \ ) 212 | else 213 | call prop_add(a:row, a:col, #{ 214 | \ length: length, 215 | \ type: a:prop_type, 216 | \ bufnr: a:bufnr, 217 | \ id: a:id, 218 | \ }) 219 | endif 220 | endfunction 221 | 222 | function ddu#ui#ff#_open_preview_window( 223 | \ params, bufnr, preview_bufnr, prev_winid, preview_winid) abort 224 | 225 | const use_winfixbuf = 226 | \ '+winfixbuf'->exists() && a:params.previewSplit !=# 'no' 227 | 228 | if a:preview_winid >= 0 && (!a:params.previewFloating || has('nvim')) 229 | call win_gotoid(a:preview_winid) 230 | 231 | if use_winfixbuf 232 | call setwinvar(a:preview_winid, '&winfixbuf', v:false) 233 | endif 234 | 235 | execute 'buffer' a:preview_bufnr 236 | 237 | if use_winfixbuf 238 | call setwinvar(a:preview_winid, '&winfixbuf', v:true) 239 | endif 240 | 241 | return a:preview_winid 242 | endif 243 | 244 | let preview_width = a:params.previewWidth 245 | let preview_height = a:params.previewHeight 246 | const winnr = a:bufnr->bufwinid() 247 | const pos = winnr->win_screenpos() 248 | const win_width = winnr->winwidth() 249 | const win_height = winnr->winheight() 250 | 251 | if a:params.previewSplit ==# 'vertical' 252 | if a:params.previewFloating 253 | let win_row = a:params.previewRow > 0 ? 254 | \ a:params.previewRow : pos[0] - 1 255 | let win_col = a:params.previewCol > 0 ? 256 | \ a:params.previewCol : pos[1] - 1 257 | 258 | if a:params.previewRow <= 0 && win_row <= preview_height 259 | let win_col += win_width 260 | if (win_col + preview_width) > &columns 261 | let win_col -= preview_width 262 | endif 263 | endif 264 | 265 | if a:params.previewCol <= 0 && a:params.previewFloatingBorder !=# 'none' 266 | let preview_width -= 1 267 | endif 268 | 269 | if has('nvim') 270 | let winopts = #{ 271 | \ relative: 'editor', 272 | \ row: win_row, 273 | \ col: win_col, 274 | \ width: preview_width, 275 | \ height: preview_height, 276 | \ border: a:params.previewFloatingBorder, 277 | \ title: a:params.previewFloatingTitle, 278 | \ title_pos: a:params.previewFloatingTitlePos, 279 | \ zindex: a:params.previewFloatingZindex, 280 | \ } 281 | const winid = nvim_open_win( 282 | \ a:preview_bufnr, a:params.previewFocusable, winopts) 283 | else 284 | const winopts = #{ 285 | \ pos: 'topleft', 286 | \ posinvert: v:false, 287 | \ line: win_row + 1, 288 | \ col: win_col + 1, 289 | \ border: [], 290 | \ borderchars: [], 291 | \ borderhighlight: [], 292 | \ highlight: 'Normal', 293 | \ maxwidth: preview_width, 294 | \ minwidth: preview_width, 295 | \ maxheight: preview_height, 296 | \ minheight: preview_height, 297 | \ scrollbar: 0, 298 | \ title: a:params.previewFloatingTitle, 299 | \ wrap: 0, 300 | \ zindex: a:params.previewFloatingZindex, 301 | \ } 302 | if a:preview_winid >= 0 303 | call popup_close(a:preview_winid) 304 | endif 305 | const winid = a:preview_bufnr->popup_create(winopts) 306 | endif 307 | else 308 | call win_gotoid(winnr) 309 | execute 'silent rightbelow vertical sbuffer' a:preview_bufnr 310 | setlocal winfixwidth 311 | execute 'vertical resize' preview_width 312 | const winid = win_getid() 313 | endif 314 | elseif a:params.previewSplit ==# 'horizontal' 315 | if a:params.previewFloating 316 | let win_row = a:params.previewRow > 0 ? 317 | \ a:params.previewRow : pos[0] - 1 318 | let win_col = a:params.previewCol > 0 ? 319 | \ a:params.previewCol : pos[1] - 1 320 | 321 | if a:params.previewRow <= 0 && a:params.previewFloatingBorder !=# 'none' 322 | let preview_height -= 1 323 | endif 324 | 325 | if has('nvim') 326 | if a:params.previewRow <= 0 && win_row <= preview_height 327 | let win_row += win_height + 1 328 | const anchor = 'NW' 329 | else 330 | const anchor = 'SW' 331 | endif 332 | 333 | let winopts = #{ 334 | \ relative: 'editor', 335 | \ anchor: anchor, 336 | \ row: win_row, 337 | \ col: win_col, 338 | \ width: preview_width, 339 | \ height: preview_height, 340 | \ border: a:params.previewFloatingBorder, 341 | \ title: a:params.previewFloatingTitle, 342 | \ title_pos: a:params.previewFloatingTitlePos, 343 | \ zindex: a:params.previewFloatingZindex, 344 | \ } 345 | const winid = nvim_open_win(a:preview_bufnr, v:true, winopts) 346 | else 347 | if a:params.previewRow <= 0 348 | let win_row -= preview_height + 2 349 | endif 350 | const winopts = #{ 351 | \ pos: 'topleft', 352 | \ posinvert: v:false, 353 | \ line: win_row + 1, 354 | \ col: win_col + 1, 355 | \ border: [], 356 | \ borderchars: [], 357 | \ borderhighlight: [], 358 | \ highlight: 'Normal', 359 | \ maxwidth: preview_width, 360 | \ minwidth: preview_width, 361 | \ maxheight: preview_height, 362 | \ minheight: preview_height, 363 | \ scrollbar: 0, 364 | \ title: a:params.previewFloatingTitle, 365 | \ wrap: 0, 366 | \ zindex: a:params.previewFloatingZindex, 367 | \ } 368 | if a:preview_winid >= 0 369 | call popup_close(a:preview_winid) 370 | endif 371 | const winid = a:preview_bufnr->popup_create(winopts) 372 | endif 373 | else 374 | " NOTE: If winHeight is bigger than `&lines / 2`, it will be resized. 375 | const maxheight = &lines * 4 / 10 376 | if preview_height > maxheight 377 | let preview_height = maxheight 378 | endif 379 | 380 | call win_gotoid(winnr) 381 | execute 'silent aboveleft sbuffer' a:preview_bufnr 382 | setlocal winfixheight 383 | execute 'resize ' .. preview_height 384 | const winid = win_getid() 385 | endif 386 | elseif a:params.previewSplit ==# 'no' 387 | call win_gotoid(a:prev_winid) 388 | execute 'buffer' a:preview_bufnr 389 | const winid = win_getid() 390 | endif 391 | 392 | " Set options 393 | if a:params.previewSplit !=# 'no' 394 | call setwinvar(winid, '&previewwindow', v:true) 395 | endif 396 | call setwinvar(winid, '&cursorline', v:false) 397 | if use_winfixbuf 398 | call setwinvar(winid, '&winfixbuf', v:true) 399 | endif 400 | 401 | return winid 402 | endfunction 403 | 404 | function ddu#ui#ff#_update_cursor() abort 405 | let b:ddu_ui_ff_cursor_pos = getcurpos() 406 | 407 | call ddu#ui#update_cursor() 408 | endfunction 409 | 410 | function ddu#ui#ff#_restore_cmdline(cmdline, cmdpos) abort 411 | call feedkeys(':' .. a:cmdline .. 412 | \ "\"->repeat(a:cmdline->strchars() - a:cmdpos + 1)) 413 | endfunction 414 | 415 | function ddu#ui#ff#_jump(winid, pattern, linenr) abort 416 | if a:pattern !=# '' 417 | call win_execute(a:winid, 418 | \ printf('call search(%s, "w")', a:pattern->string())) 419 | endif 420 | 421 | if a:linenr > 0 422 | call win_execute(a:winid, 423 | \ printf('call cursor(%d, 0)', a:linenr)) 424 | endif 425 | 426 | if a:pattern !=# '' || a:linenr > 0 427 | call win_execute(a:winid, 'normal! zv') 428 | call win_execute(a:winid, 'normal! zz') 429 | endif 430 | endfunction 431 | 432 | let s:cursor_text = '' 433 | let s:auto_action = {} 434 | function ddu#ui#ff#_do_auto_action() abort 435 | call s:stop_debounce_timer('s:debounce_auto_action_timer') 436 | 437 | if empty(s:auto_action) 438 | return 439 | endif 440 | 441 | if mode() ==# 'c' 442 | " NOTE: In command line mode, timer_start() does not work 443 | call s:do_auto_action() 444 | else 445 | let s:debounce_auto_action_timer = timer_start( 446 | \ s:auto_action.delay, { -> s:do_auto_action() }) 447 | endif 448 | endfunction 449 | function ddu#ui#ff#_reset_auto_action() abort 450 | let s:cursor_text = '' 451 | let s:auto_action = {} 452 | 453 | call s:stop_debounce_timer('s:debounce_auto_action_timer') 454 | 455 | augroup ddu-ui-ff-auto_action 456 | autocmd! 457 | augroup END 458 | endfunction 459 | function ddu#ui#ff#_set_auto_action(winid, auto_action) abort 460 | const prev_winid = win_getid() 461 | let s:auto_action = a:auto_action 462 | let s:auto_action.bufnr = '%'->bufnr() 463 | 464 | call win_gotoid(a:winid) 465 | 466 | " NOTE: In action execution, auto action should be skipped 467 | augroup ddu-ui-ff-auto_action 468 | autocmd CursorMoved ++nested 469 | \ : if !g:->get('ddu#ui#ff#_in_action', v:false) 470 | \ | call ddu#ui#ff#_do_auto_action() 471 | \ | endif 472 | augroup END 473 | 474 | call win_gotoid(prev_winid) 475 | endfunction 476 | 477 | function s:do_auto_action() abort 478 | const bufnr = '%'->bufnr() 479 | if bufnr != s:auto_action.bufnr 480 | return 481 | endif 482 | 483 | const text = bufnr->getbufline(win_getid()->getcurpos()[1])->get(0, '') 484 | if text ==# s:cursor_text 485 | return 486 | endif 487 | if text ==# s:cursor_text 488 | return 489 | endif 490 | 491 | if s:auto_action.sync 492 | call ddu#ui#sync_action(s:auto_action.name, s:auto_action.params) 493 | else 494 | call ddu#ui#do_action(s:auto_action.name, s:auto_action.params) 495 | endif 496 | let s:cursor_text = text 497 | endfunction 498 | 499 | function s:stop_debounce_timer(timer_name) abort 500 | if a:timer_name->exists() 501 | silent! call timer_stop({a:timer_name}) 502 | unlet {a:timer_name} 503 | endif 504 | endfunction 505 | 506 | function ddu#ui#ff#_truncate(str, max, footer_width, separator) abort 507 | const width = a:str->strwidth() 508 | if width <= a:max 509 | const ret = a:str 510 | else 511 | const header_width = a:max - a:separator->strwidth() - a:footer_width 512 | const ret = s:strwidthpart(a:str, header_width) .. a:separator 513 | \ .. s:strwidthpart_reverse(a:str, a:footer_width) 514 | endif 515 | return s:truncate(ret, a:max) 516 | endfunction 517 | function s:truncate(str, width) abort 518 | " Original function is from mattn. 519 | " http://github.com/mattn/googlereader-vim/tree/master 520 | 521 | if a:str =~# '^[\x00-\x7f]*$' 522 | return a:str->len() < a:width 523 | \ ? printf('%-' .. a:width .. 's', a:str) 524 | \ : a:str->strpart(0, a:width) 525 | endif 526 | 527 | let ret = a:str 528 | let width = a:str->strwidth() 529 | if width > a:width 530 | let ret = s:strwidthpart(ret, a:width) 531 | let width = ret->strwidth() 532 | endif 533 | 534 | return ret 535 | endfunction 536 | function s:strwidthpart(str, width) abort 537 | const str = a:str->tr("\t", ' ') 538 | const vcol = a:width + 2 539 | return str->matchstr('.*\%<' .. (vcol < 0 ? 0 : vcol) .. 'v') 540 | endfunction 541 | function s:strwidthpart_reverse(str, width) abort 542 | const str = a:str->tr("\t", ' ') 543 | const vcol = str->strwidth() - a:width 544 | return str->matchstr('\%>' .. (vcol < 0 ? 0 : vcol) .. 'v.*') 545 | endfunction 546 | -------------------------------------------------------------------------------- /doc/ddu-ui-ff.txt: -------------------------------------------------------------------------------- 1 | *ddu-ui-ff.txt* Fuzzy finder UI for ddu.vim 2 | 3 | Author: Shougo 4 | License: MIT license 5 | 6 | CONTENTS *ddu-ui-ff-contents* 7 | 8 | Introduction |ddu-ui-ff-introduction| 9 | Install |ddu-ui-ff-install| 10 | Interface |ddu-ui-ff-interface| 11 | Actions |ddu-ui-ff-actions| 12 | Params |ddu-ui-ff-params| 13 | Params expression |ddu-ui-ff-params-expression| 14 | Examples |ddu-ui-ff-examples| 15 | FAQ |ddu-ui-ff-faq| 16 | Compatibility |ddu-ui-ff-compatibility| 17 | 18 | 19 | ============================================================================== 20 | INTRODUCTION *ddu-ui-ff-introduction* 21 | 22 | This UI is standard fuzzy finder. 23 | 24 | 25 | ============================================================================== 26 | INSTALL *ddu-ui-ff-install* 27 | 28 | Please install both "ddu.vim" and "denops.vim". 29 | 30 | https://github.com/Shougo/ddu.vim 31 | https://github.com/vim-denops/denops.vim 32 | 33 | 34 | ============================================================================== 35 | INTERFACE *ddu-ui-ff-interface* 36 | 37 | ------------------------------------------------------------------------------ 38 | ACTIONS *ddu-ui-ff-actions* 39 | 40 | *ddu-ui-ff-action-checkItems* 41 | checkItems 42 | Check the items are updated. 43 | NOTE: Source support is needed for the feature. 44 | 45 | *ddu-ui-ff-action-chooseAction* 46 | chooseAction 47 | Choose and fire the action by ddu UI. 48 | NOTE: Quit the UI after executing the action. 49 | 50 | NOTE: "ddu-source-action" is required. 51 | https://github.com/Shougo/ddu-source-action 52 | 53 | *ddu-ui-ff-action-chooseInput* 54 | chooseInput 55 | Choose |ddu-option-input| by ddu UI. 56 | 57 | NOTE: "ddu-source-input_history" is required. 58 | https://github.com/Shougo/ddu-source-input_history 59 | 60 | *ddu-ui-ff-action-clearSelectAllItems* 61 | clearSelectAllItems 62 | Clear all selected items. 63 | 64 | *ddu-ui-ff-action-closePreviewWindow* 65 | closePreviewWindow 66 | Close the preview window. 67 | 68 | *ddu-ui-ff-action-collapseItem* 69 | collapseItem 70 | Collapse the item tree. 71 | If the item is already collapsed, the parent item is used. 72 | 73 | *ddu-ui-ff-action-cursorNext* 74 | cursorNext 75 | params: 76 | {count}: Move count 77 | {loop}: Loop the cursor 78 | 79 | Move the cursor to the next. 80 | 81 | *ddu-ui-ff-action-cursorPrevious* 82 | cursorPrevious 83 | params: 84 | {count}: Move count 85 | {loop}: Loop the cursor 86 | 87 | Move the cursor to the previous. 88 | 89 | *ddu-ui-ff-action-cursorTreeBottom* 90 | cursorTreeBottom 91 | 92 | Move the cursor to the bottom of current tree. 93 | 94 | *ddu-ui-ff-action-cursorTreeTop* 95 | cursorTreeTop 96 | 97 | Move the cursor to the top of current tree. 98 | 99 | *ddu-ui-ff-action-expandItem* 100 | expandItem 101 | params: 102 | {mode}: The supported values are: 103 | 104 | "toggle": 105 | Close the item tree if the directory 106 | is opened. 107 | 108 | {maxLevel}: 109 | Maximum expand recursive level. 110 | If it is less than 0, infinite recursive. 111 | 112 | {isGrouped}: 113 | If it is only one item tree, it will be 114 | concatenated. 115 | 116 | {isInTree}: 117 | If it is not empty tree, enter the tree. 118 | 119 | Expand the item tree. 120 | 121 | *ddu-ui-ff-action-getItem* 122 | getItem 123 | Set cursor item to "b:ddu_ui_item" variable. 124 | 125 | *ddu-ui-ff-action-getSelectedItems* 126 | getSelectedItems 127 | Set selected items to "b:ddu_ui_selected_items" variable. 128 | 129 | *ddu-ui-ff-action-inputAction* 130 | inputAction 131 | Prompt for a name using |input()| and fire the action with it. 132 | 133 | *ddu-ui-ff-action-itemAction* 134 | itemAction 135 | params: 136 | {name}: Action name 137 | {params}: Action params dictionary 138 | 139 | Close the UI window and fire {name} action for selected or 140 | current cursor items. 141 | You can find the actions list in item's kind documentation. 142 | If {name} is empty, "default" will be used. 143 | 144 | NOTE: You cannot mix multiple kinds/sources. 145 | 146 | *ddu-ui-ff-action-openFilterWindow* 147 | openFilterWindow 148 | Open the filter window in command line. 149 | 150 | params: 151 | {input}: Overwrite current input 152 | 153 | *ddu-ui-ff-action-preview* 154 | preview 155 | params: 156 | {syntaxLimitChars}: Max number of chars to apply 157 | 'syntax' and 'filetype' to 158 | previewed contents. 159 | (Default: 400000) 160 | 161 | The remaining params are passed to |ddu-kinds|. 162 | See also |ddu-kind-attribute-getPreviewer| and kinds 163 | documentation. 164 | 165 | Preview the item in preview window. 166 | 167 | *ddu-ui-ff-action-previewExecute* 168 | previewExecute 169 | params: 170 | {command}: Command to execute 171 | 172 | Execute command in preview window. 173 | 174 | *ddu-ui-ff-action-previewPath* 175 | previewPath 176 | Preview the item path in echo area. 177 | NOTE: 'cmdheight' must be greater than 0. 178 | 179 | *ddu-ui-ff-action-quit* 180 | quit 181 | Quit the UI window. 182 | 183 | params: 184 | {force}: Force quit buffer if it is non zero. 185 | NOTE: You can't resume the buffer. 186 | 187 | *ddu-ui-ff-action-redraw* 188 | redraw 189 | params: 190 | {method}: Redraw method 191 | 192 | "refreshItems": Gather all source items and execute 193 | "uiRefresh". 194 | "uiRefresh": Refresh UI items and execute "uiRedraw". 195 | (Default) 196 | "uiRedraw": Redraw current UI window. 197 | 198 | Redraw the UI. 199 | NOTE: The preview window is closed when |ddu-ui-ff-params| is 200 | changed. 201 | 202 | *ddu-ui-ff-action-toggleAllItems* 203 | toggleAllItems 204 | Toggle selected state for the all items. 205 | 206 | *ddu-ui-ff-action-toggleAutoAction* 207 | toggleAutoAction 208 | Toggle auto action state(enabled/disabled). 209 | 210 | *ddu-ui-ff-action-togglePreview* 211 | togglePreview 212 | Toggle |ddu-ui-ff-action-preview| for cursor item. 213 | 214 | *ddu-ui-ff-action-toggleSelectItem* 215 | toggleSelectItem 216 | Toggle selected state for cursor item. 217 | 218 | *ddu-ui-ff-action-updateOptions* 219 | updateOptions 220 | params: 221 | {option-name}: Option name and value 222 | 223 | Update current options. Refer to |ddu-options| about options. 224 | NOTE: It does not redraw items. 225 | NOTE: If you execute it in the action, |ddu-options| is not 226 | updated in current context. 227 | 228 | ------------------------------------------------------------------------------ 229 | PARAMS *ddu-ui-ff-params* 230 | 231 | *ddu-ui-ff-param-autoAction* 232 | autoAction (dictionary) 233 | If it is specified, the UI action is executed when the cursor 234 | is moved. It has the following keys. 235 | NOTE: If you want to enable autoAction when UI started, you 236 | must set |ddu-ui-ff-param-startAutoAction|. 237 | 238 | delay (number) (Optional) 239 | Time in milliseconds to delay the auto action. 240 | If you feel slow, specify large value. 241 | Set 0 to disable debouncing. 242 | NOTE: It does not work in the filter window. 243 | 244 | Default: 10 245 | 246 | name (string) (Required) 247 | Action name 248 | 249 | params (dictionary) (Optional) 250 | Action params 251 | 252 | Default: {} 253 | 254 | sync (boolean) (Optional) 255 | If it is true, action is executed synchronously. 256 | NOTE: If it is false, the screen may be flickered. 257 | 258 | Default: v:true 259 | 260 | *ddu-ui-ff-param-autoResize* 261 | autoResize (boolean) 262 | Auto resize the window height automatically. 263 | 264 | Default: v:false 265 | 266 | *ddu-ui-ff-param-cursorPos* 267 | cursorPos (number) 268 | Select {number} candidate. It is 1 origin. 269 | If you set the option, cursor restore feature is disabled. 270 | 271 | Default: 0 272 | 273 | *ddu-ui-ff-param-displaySourceName* 274 | displaySourceName (string) 275 | Display source name in the buffer. Following values are 276 | available: 277 | 278 | "long": display full source name 279 | "short": display shorter source name 280 | "no": does not display 281 | 282 | Default: "no" 283 | 284 | *ddu-ui-ff-param-displayTree* 285 | displayTree (boolean) 286 | Display tree structure. 287 | NOTE: To use the feature, the sources support tree structure. 288 | 289 | Default: v:false 290 | 291 | *ddu-ui-ff-param-exprParams* 292 | exprParams (string[]) 293 | Evaluate params list. 294 | If the param is string, it is evaluated as 295 | |ddu-ui-ff-params-expression|. 296 | 297 | Default: [ 298 | "previewCol", 299 | "previewRow", 300 | "previewHeight", 301 | "previewWidth", 302 | "winCol", 303 | "winRow", 304 | "winHeight", 305 | "winWidth", 306 | ] 307 | 308 | *ddu-ui-ff-param-floatingBorder* 309 | floatingBorder (string | list) 310 | Specify the style of the window border if 311 | |ddu-ui-ff-param-split| is "floating". 312 | See |nvim_open_win()| or |popup_create-arguments| for the 313 | detail. 314 | 315 | Following values are available: 316 | 317 | "none": Disabled. 318 | "single": A single line box. 319 | "double": A double line box. 320 | "rounded": Neovim only. 321 | "solid": Neovim only. 322 | "shadow": Neovim only. 323 | array: Specifify the eight chars building up the border. 324 | 325 | Default: "none" 326 | 327 | *ddu-ui-ff-param-floatingTitle* 328 | floatingTitle (string | list) 329 | Specify the title of the window border if 330 | |ddu-ui-ff-param-floatingBorder| is not "none". 331 | 332 | Default: "" 333 | 334 | *ddu-ui-ff-param-floatingTitlePos* 335 | floatingTitlePos (string) 336 | Specify the title position of the window border if 337 | |ddu-ui-ff-param-floatingBorder| is not "none". 338 | See |nvim_open_win()| for the detail. 339 | NOTE: It is Neovim only. 340 | 341 | Default: "left" 342 | 343 | *ddu-ui-ff-param-focus* 344 | focus (boolean) 345 | Focus on the UI window after opening the UI window. 346 | 347 | Default: v:true 348 | 349 | *ddu-ui-ff-param-highlights* 350 | highlights (dictionary) 351 | It specifies ddu-ui-ff buffer highlights. 352 | It can contain following keys 353 | 354 | filterText (string) 355 | Specify filter text highlight. 356 | Default: "Normal" 357 | 358 | floating (string) 359 | Specify floating window background highlight. 360 | Default: "NormalFloat" 361 | 362 | floatingBorder (string) 363 | Specify border highlight of flowing window 364 | Default: "FloatBorder" 365 | 366 | floatingCursorLine (string) 367 | Specify cursor line highlight of floating window 368 | Default: "CursorLine" 369 | 370 | preview (string) 371 | Specify preview window highlight. 372 | Default: "Search" 373 | 374 | selected (string) 375 | Specify selected item highlight. 376 | Default: "Statement" 377 | 378 | Default: {} 379 | 380 | *ddu-ui-ff-param-ignoreEmpty* 381 | ignoreEmpty (boolean) 382 | Don't open the UI window if the items are empty. 383 | NOTE: It works only if the UI window is not visible. 384 | 385 | Default: v:false 386 | 387 | *ddu-ui-ff-param-immediateAction* 388 | immediateAction (string) 389 | If it is not empty and the number of item is exactly one, it 390 | runs |ddu-ui-ff-action-itemAction| immediately. 391 | 392 | NOTE: You need to set |ddu-option-sync|. Because UI window 393 | may be created before all sources are finished. 394 | NOTE: It works only if the UI window is not visible. 395 | 396 | Default: "" 397 | 398 | *ddu-ui-ff-param-maxDisplayItems* 399 | maxDisplayItems (number) 400 | The maximum number of displayed items. 401 | NOTE: If you increase the param, the UI will be slower. 402 | 403 | Default: 1000 404 | 405 | *ddu-ui-ff-param-maxHighlightItems* 406 | maxHighlightItems (number) 407 | The maximum number of highlighted items. 408 | NOTE: If you increase the param, the UI will be slower. 409 | 410 | Default: 100 411 | 412 | *ddu-ui-ff-param-maxWidth* 413 | maxWidth (number) 414 | The maximum number of width. 415 | 416 | Default: 200 417 | 418 | *ddu-ui-ff-param-onPreview* 419 | onPreview (function) 420 | It is called when |ddu-ui-ff-action-preview| is fired. 421 | NOTE: The function must be registered by 422 | |denops#callback#register()|. 423 | NOTE: Current window may not be the preview window. 424 | 425 | *ddu-ui-ff-param-overwriteStatusline* 426 | overwriteStatusline (boolean) 427 | If it is true, the original 'statusline' value is set on the 428 | buffer. 429 | 430 | Default: v:true 431 | 432 | *ddu-ui-ff-param-overwriteTitle* 433 | overwriteTitle (boolean) 434 | If it is true, the original 'titlestring' value is set on the 435 | buffer. 436 | 437 | Default: 438 | v:false 439 | v:true (When 'laststatus' is 0) 440 | 441 | *ddu-ui-ff-param-pathFilter* 442 | pathFilter (string) 443 | Filter regexp string for path items. 444 | NOTE: It is JavaScript regexp. 445 | 446 | Default: "" 447 | 448 | *ddu-ui-ff-param-previewCol* 449 | previewCol (number) 450 | Set the column position of the preview window if 451 | |ddu-ui-ff-param-previewFloating| is v:true. 452 | 453 | Default: 0 454 | 455 | *ddu-ui-ff-param-previewFloating* 456 | previewFloating (boolean) 457 | Use floating window in |ddu-ui-ff-action-preview|. 458 | 459 | Default: v:false 460 | 461 | *ddu-ui-ff-param-previewFloatingBorder* 462 | previewFloatingBorder (string | list) 463 | Specify the style of the preview window border if 464 | |ddu-ui-ff-param-previewFloating| is v:true. 465 | See |nvim_open_win()| for the detail. 466 | NOTE: It is Neovim only. 467 | 468 | Default: "none" 469 | 470 | *ddu-ui-ff-param-previewFloatingTitle* 471 | previewFloatingTitle (string | list) 472 | Specify the title of the preview floating window with border 473 | if |ddu-ui-ff-param-previewFloatingBorder| is not "none". 474 | 475 | Default: "" 476 | 477 | *ddu-ui-ff-param-previewFloatingTitlePos* 478 | previewFloatingTitlePos (string) 479 | Specify the title position of the preview floating window with 480 | border if |ddu-ui-ff-param-previewFloatingBorder| is not 481 | "none". 482 | 483 | Default: "left" 484 | 485 | *ddu-ui-ff-param-previewFloatingZindex* 486 | previewFloatingZindex (number) 487 | Specify the style of the preview window zindex if 488 | |ddu-ui-ff-param-split| is "floating". 489 | 490 | Default: 100 491 | 492 | *ddu-ui-ff-param-previewFocusable* 493 | previewFocusable (boolean) 494 | Focusable preview window in |ddu-ui-ff-action-preview| if 495 | |ddu-ui-ff-param-previewFloating| is v:true. 496 | NOTE: It is Neovim only. 497 | 498 | Default: v:true 499 | 500 | *ddu-ui-ff-param-previewHeight* 501 | previewHeight (number) 502 | Set the height of the |preview-window| in 503 | |ddu-ui-ff-action-preview|. 504 | If |ddu-ui-ff-param-previewFloating|, set the height of the 505 | floating window. 506 | NOTE: If |ddu-ui-ff-param-previewSplit| is "horizontal", the 507 | value must be less than `&lines - 2`. 508 | 509 | Default: 10 510 | 511 | *ddu-ui-ff-param-previewMaxSize* 512 | previewMaxSize (number) 513 | Set the maximum file size for preview. 514 | 515 | Default: 1000000 516 | 517 | *ddu-ui-ff-param-previewRow* 518 | previewRow (number) 519 | Set the row position of the preview window if 520 | |ddu-ui-ff-param-previewFloating| is v:true. 521 | 522 | Default: 0 523 | 524 | *ddu-ui-ff-param-previewSplit* 525 | previewSplit (string) 526 | Specify preview split mode in |ddu-ui-ff-action-preview|. 527 | 528 | Following values are available: 529 | 530 | "horizontal": 531 | horizontal split, |ddu-ui-ff-param-winWidth| is 532 | used. 533 | "vertical": 534 | vertical split, |ddu-ui-ff-param-winHeight| is 535 | used. 536 | "no": 537 | no split 538 | 539 | Default: "horizontal" 540 | 541 | *ddu-ui-ff-param-previewWidth* 542 | previewWidth (number) 543 | Set the width of the |preview-window| in 544 | |ddu-ui-ff-action-preview|. 545 | If |ddu-ui-ff-param-previewFloating|, set the width of the 546 | floating window. 547 | 548 | Default: 80 549 | 550 | *ddu-ui-ff-param-previewWindowOptions* 551 | previewWindowOptions (list) 552 | Set the window options of the |preview-window| in 553 | |ddu-ui-ff-action-preview|. 554 | If |ddu-ui-ff-param-previewFloating| is set, set the options 555 | of the floating window. 556 | See |options| for the detail. 557 | NOTE: The options are applied in the array order. 558 | 559 | Default: 560 | 561 | [ 562 | ["&signcolumn", "no"], 563 | ["&foldcolumn", 0], 564 | ["&foldenable", 0], 565 | ["&number", 0], 566 | ["&wrap", 0], 567 | ] 568 | 569 | *ddu-ui-ff-param-replaceCol* 570 | replaceCol (number) 571 | Set the column position of the replace current text after 572 | actions. 573 | It is useful for insert/command mode. 574 | 575 | Default: 0 576 | 577 | *ddu-ui-ff-param-reversed* 578 | reversed (boolean) 579 | Display the items in reversed order. 580 | NOTE: It may increase screen flicker. Because the cursor must 581 | be moved if you narrowing text. 582 | 583 | Default: v:false 584 | 585 | *ddu-ui-ff-param-split* 586 | split (string) 587 | Specify split mode. 588 | 589 | Following values are available: 590 | 591 | "horizontal": horizontal split 592 | "vertical": vertical split 593 | "floating": use floating window feature 594 | "tab": use new tab 595 | "no": no split 596 | 597 | NOTE: "floating" does not work in Vim. 598 | 599 | Default: "horizontal" 600 | 601 | *ddu-ui-ff-param-splitDirection* 602 | splitDirection (string) 603 | Specify split direction. 604 | 605 | Default: "botright" 606 | 607 | *ddu-ui-ff-param-startAutoAction* 608 | startAutoAction (boolean) 609 | If it is true, |ddu-ui-ff-param-autoAction| is fired 610 | automatically. 611 | Note: It must be set before UI initialization. 612 | 613 | Default: v:false 614 | 615 | *ddu-ui-ff-param-winCol* 616 | winCol (number | string) 617 | Set the column position of the window if 618 | |ddu-ui-ff-param-split| is "floating". 619 | 620 | Default: "(&columns - eval(uiParams.winWidth)) / 2" 621 | 622 | *ddu-ui-ff-param-winHeight* 623 | winHeight (number | string) 624 | Set the height of the window if |ddu-ui-ff-param-split| is 625 | "horizontal". 626 | If |ddu-ui-ff-param-split| is "floating", 627 | set the height of the floating window. 628 | NOTE: If |ddu-ui-ff-param-split| is "horizontal", the value 629 | must be less than `&lines - 2`. 630 | 631 | Default: 20 632 | 633 | *ddu-ui-ff-param-winRow* 634 | winRow (number | string) 635 | Set the row position of the window if |ddu-ui-ff-param-split| 636 | is "floating". 637 | 638 | Default: "&lines / 2 - 10" 639 | 640 | *ddu-ui-ff-param-winWidth* 641 | winWidth (number | string) 642 | Set the width of the window if |ddu-ui-ff-param-split| is 643 | "vertical". 644 | If |ddu-ui-ff-param-split| is "floating", set the width of 645 | the floating window. 646 | 647 | Default: "&columns / 2" 648 | 649 | ------------------------------------------------------------------------------ 650 | PARAMS EXPRESSION *ddu-ui-ff-params-expression* 651 | 652 | If the parameter value is a string, it can be evaluated as a Vim |expression|. 653 | Expressions must not have side effects. The following variables exist in the 654 | expression context: 655 | 656 | bufName (string) 657 | bufNr (number) 658 | itemCount (number) 659 | sources (string[]) 660 | uiParams (|ddu-ui-ff-params|) 661 | winId (number) 662 | 663 | An example of floating the UI window on the left side and the preview window 664 | on the right side of the screen: >vim 665 | 666 | call ddu#custom#patch_global(#{ 667 | \ ui: 'ff', 668 | \ uiParams: #{ 669 | \ ff: #{ 670 | \ split: 'floating', 671 | \ winHeight: '&lines - 8', 672 | \ winWidth: '&columns / 2 - 2', 673 | \ winRow: 1, 674 | \ winCol: 1, 675 | \ previewFloating: v:true, 676 | \ previewHeight: '&lines - 8', 677 | \ previewWidth: '&columns / 2 - 2', 678 | \ previewRow: 1, 679 | \ previewCol: '&columns / 2 + 1', 680 | \ } 681 | \ }, 682 | \ }) 683 | < 684 | 685 | ============================================================================== 686 | EXAMPLES *ddu-ui-ff-examples* 687 | >vim 688 | call ddu#custom#patch_global(#{ 689 | \ ui: 'ff', 690 | \ }) 691 | 692 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 693 | function s:ddu_ff_my_settings() abort 694 | nnoremap 695 | \ call ddu#ui#do_action('itemAction') 696 | nnoremap 697 | \ call ddu#ui#do_action('toggleSelectItem') 698 | nnoremap i 699 | \ call ddu#ui#do_action('openFilterWindow') 700 | nnoremap q 701 | \ call ddu#ui#do_action('quit') 702 | endfunction 703 | < 704 | 705 | ============================================================================== 706 | FREQUENTLY ASKED QUESTIONS (FAQ) *ddu-ui-ff-faq* 707 | 708 | FAQ 1: |ddu-ui-ff-faq-1| 709 | I want to toggle hidden files by mappings. 710 | 711 | FAQ 2: |ddu-ui-ff-faq-2| 712 | I want to call default action in the filter window. 713 | 714 | FAQ 3: |ddu-ui-ff-faq-3| 715 | I want to move the cursor in the filter window, while in insert mode. 716 | 717 | FAQ 4: |ddu-ui-ff-faq-4| 718 | I want to define |ddu-option-name| depend key mappings. 719 | 720 | FAQ 5: |ddu-ui-ff-faq-5| 721 | I want to define kind depend key mappings. 722 | 723 | FAQ 6: |ddu-ui-ff-faq-6| 724 | I want to pass params to the action when 725 | |ddu-ui-ff-action-chooseAction|. 726 | 727 | FAQ 7: |ddu-ui-ff-faq-7| 728 | I want to use Vim syntax highlight in the buffer. 729 | 730 | FAQ 8: |ddu-ui-ff-faq-8| 731 | I want to custom the statusline. 732 | 733 | FAQ 9: |ddu-ui-ff-faq-9| 734 | I want to move to line quickly like denite.nvim's "quick-move" 735 | feature. 736 | 737 | FAQ 10: |ddu-ui-ff-faq-10| 738 | Why ddu-ui-ff does not support Vim's popup window feature instead of 739 | Neovim's floating window feature? 740 | 741 | FAQ 11: |ddu-ui-ff-faq-11| 742 | I want to toggle selected items in visual mode. 743 | 744 | FAQ 12: |ddu-ui-ff-faq-12| 745 | I want to use "ddu-ui-ff" in insert mode. 746 | 747 | FAQ 13: |ddu-ui-ff-faq-13| 748 | I want to use "ddu-ui-ff" in command line mode. 749 | 750 | FAQ 14: |ddu-ui-ff-faq-14| 751 | I want to switch sources without clear current filter input. 752 | 753 | FAQ 15: |ddu-ui-ff-faq-15| 754 | I want to use existing buffer to show preview. 755 | 756 | FAQ 16: |ddu-ui-ff-faq-16| 757 | ":UniteNext"/":UnitePrevious"/":Denite -resume -cursor-pos=+1 758 | -immediately" like commands are available? 759 | 760 | FAQ 17: |ddu-ui-ff-faq-17| 761 | I want to display cursor mark on the left of ddu window like 762 | "telescope.nvim". 763 | 764 | FAQ 18: |ddu-ui-ff-faq-18| 765 | How do I set the floating title with list style ? 766 | 767 | FAQ 19: |ddu-ui-ff-faq-19| 768 | When calling |ddu-ui-ff-action-quit| or |ddu-ui-ff-action-itemAction| 769 | from filter window, it remains filter window. 770 | 771 | FAQ 20: |ddu-ui-ff-faq-20| 772 | I cannot see what item is selected in the filter window. 773 | 774 | FAQ 21: |ddu-ui-ff-faq-21| 775 | I want to change preview cursor on the top. 776 | 777 | FAQ 22: |ddu-ui-ff-faq-22| 778 | I want to scroll preview window. 779 | 780 | FAQ 23: |ddu-ui-ff-faq-23| 781 | I want to preview selected item automatically. 782 | 783 | FAQ 24: |ddu-ui-ff-faq-24| 784 | I want to start filter window when UI is initialized. 785 | 786 | FAQ 25: |ddu-ui-ff-faq-25| 787 | I want to change filter window position. 788 | 789 | FAQ 26: |ddu-ui-ff-faq-26| 790 | The filter input is cleared when I press key. 791 | 792 | FAQ 27: |ddu-ui-ff-faq-27| 793 | The cursor is not restored when the items are refreshed. 794 | 795 | ------------------------------------------------------------------------------ 796 | *ddu-ui-ff-faq-1* 797 | Q: I want to start narrowing in the first. 798 | 799 | A: You can use |input()| like this. 800 | > 801 | call ddu#start(#{ input: 'Pattern:'->input() }) 802 | < 803 | *ddu-ui-ff-faq-2* 804 | Q: I want to call default action in the filter window. 805 | 806 | A: >vim 807 | autocmd User Ddu:uiOpenFilterWindow 808 | \ call s:ddu_filter_my_settings() 809 | function s:ddu_filter_my_settings() abort 810 | let s:save_cr = ''->maparg('c', v:false, v:true) 811 | 812 | cnoremap 813 | \ call ddu#ui#do_action('itemAction') 814 | endfunction 815 | autocmd User Ddu:uiCloseFilterWindow 816 | \ call s:ddu_filter_cleanup() 817 | function s:ddu_filter_cleanup() abort 818 | if s:save_cr->empty() 819 | cunmap 820 | else 821 | call mapset('c', 0, s:save_cr) 822 | endif 823 | endfunction 824 | < 825 | 826 | NOTE: You must restore keys when 827 | |ddu-autocmd-Ddu:uiCloseFilterWindow|. 828 | 829 | *ddu-ui-ff-faq-3* 830 | Q: I want to move the cursor in the filter window, while in insert mode. 831 | 832 | A: Really? It is not the Vim way to move the cursor while in insert mode. You 833 | must force this behaviour. 834 | >vim 835 | autocmd User Ddu:uiOpenFilterWindow 836 | \ call s:ddu_filter_my_settings() 837 | function s:ddu_filter_my_settings() abort 838 | set cursorline 839 | 840 | call ddu#ui#save_cmaps(['', '']) 841 | 842 | cnoremap 843 | \ call ddu#ui#do_action('cursorNext') 844 | cnoremap 845 | \ call ddu#ui#do_action('cursorPrevious') 846 | endfunction 847 | 848 | autocmd User Ddu:uiCloseFilterWindow 849 | \ call s:ddu_filter_cleanup() 850 | function s:ddu_filter_cleanup() abort 851 | set nocursorline 852 | 853 | call ddu#ui#restore_cmaps() 854 | endfunction 855 | < 856 | 857 | If you want to loop the cursor: 858 | NOTE: It does not support |ddu-ui-ff-param-reversed|. 859 | >vim 860 | autocmd User Ddu:uiOpenFilterWindow 861 | \ call s:ddu_filter_my_settings() 862 | function s:ddu_filter_my_settings() abort 863 | setlocal cursorline 864 | 865 | call ddu#ui#save_cmaps(['', '']) 866 | 867 | cnoremap 868 | \ call ddu#ui#do_action('cursorNext', 869 | \ #{ loop: v:true }) 870 | cnoremap 871 | \ call ddu#ui#do_action('cursorPrevious', 872 | \ #{ loop: v:true }) 873 | endfunction 874 | < 875 | *ddu-ui-ff-faq-4* 876 | Q: I want to define |ddu-option-name| depend key mappings. 877 | 878 | A: You can use "b:ddu_ui_name". >vim 879 | 880 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 881 | function s:ddu_ff_my_settings() abort 882 | if b:ddu_ui_name ==# 'foo' 883 | nnoremap e 884 | \ call ddu#ui#do_action('itemAction', {'name': 'edit'}) 885 | endif 886 | endfunction 887 | < 888 | *ddu-ui-ff-faq-5* 889 | Q: I want to define kind depend key mappings. 890 | 891 | A: You can use |ddu#ui#get_item()|. >vim 892 | 893 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 894 | function s:ddu_ff_my_settings() abort 895 | nnoremap e 896 | \ call ddu#ui#do_action('itemAction', 897 | \ ddu#ui#get_item()->get('kind', '') ==# 'file' ? 898 | \ {'name': 'edit'} : {'name': 'open'}) 899 | endfunction 900 | < 901 | *ddu-ui-ff-faq-6* 902 | Q: I want to pass params to the action when |ddu-ui-ff-action-chooseAction|. 903 | 904 | A: You can pass params like this. >vim 905 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 906 | function s:ddu_ff_my_settings() abort 907 | nnoremap E 908 | \ call ddu#ui#do_action('itemAction', 909 | \ {'params': eval(input('params: '))}) 910 | endfunction 911 | < 912 | *ddu-ui-ff-faq-7* 913 | Q: I want to use Vim syntax highlight in the buffer. 914 | 915 | A: Please set 'syntax' option in |FileType| autocmd. 916 | If you want to use |treesitter|, you need to stop it manually after quit. >vim 917 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 918 | function s:ddu_ff_my_settings() abort 919 | if b:ddu_ui_name ==# 'vim' 920 | setlocal syntax=vim 921 | " Or 922 | " lua vim.treesitter.start(nil, 'vim') 923 | " autocmd WinClosed ++once lua vim.treesitter.stop() 924 | endif 925 | endfunction 926 | < 927 | *ddu-ui-ff-faq-8* 928 | Q: I want to custom the statusline. 929 | 930 | A: You can disable the original statusline by |ddu-ui-ff-param-statusline|. 931 | And you can get the status line information by "w:ddu_ui_ff_status" variable. 932 | 933 | *ddu-ui-ff-faq-9* 934 | Q: I want to move to line quickly like denite.nvim's "quick-move" feature. 935 | 936 | A: Please use qselect.vim for it. 937 | https://github.com/Shougo/qselect.vim 938 | 939 | *ddu-ui-ff-faq-10* 940 | Q: Why ddu-ui-ff does not support Vim's popup window feature instead of 941 | Neovim's floating window feature? 942 | https://github.com/Shougo/ddu-ui-ff/issues/48 943 | 944 | A: Because Vim's popup window feature is not focusable. 945 | ddu-ui-ff implementation depends on the feature. So the support is not 946 | acceptable. 947 | 948 | *ddu-ui-ff-faq-11* 949 | Q: I want to toggle selected items in visual mode. 950 | 951 | A: >vim 952 | xnoremap 953 | \ :call ddu#ui#do_action('toggleSelectItem') 954 | < 955 | *ddu-ui-ff-faq-12* 956 | Q: I want to use "ddu-ui-ff" in insert mode. 957 | NOTE: It is experimental feature. 958 | 959 | A: >vim 960 | inoremap call ddu#start(#{ 961 | \ name: 'file', 962 | \ ui: 'ff', 963 | \ input: matchstr(getline('.')[: col('.') - 1], '\f*$'), 964 | \ sources: [ 965 | \ #{ name: 'file', options: #{ defaultAction: 'feedkeys' } }, 966 | \ ], 967 | \ uiParams: #{ 968 | \ ff: #{ 969 | \ replaceCol: match(getline('.')[: col('.') - 1], '\f*$') + 1, 970 | \ }, 971 | \ }, 972 | \ }) 973 | < 974 | *ddu-ui-ff-faq-13* 975 | Q: I want to use "ddu-ui-ff" in command line mode. 976 | NOTE: It is experimental feature. 977 | 978 | A: >vim 979 | cnoremap call ddu#start(#{ 980 | \ name: 'file', 981 | \ ui: 'ff', 982 | \ input: matchstr(getcmdline()[: getcmdpos() - 2], '\f*$'), 983 | \ sources: [ 984 | \ #{ name: 'file', options: #{ defaultAction: 'feedkeys' } }, 985 | \ ], 986 | \ uiParams: #{ 987 | \ ff: #{ 988 | \ replaceCol: match(getcmdline()[: getcmdpos() - 2], 989 | \ '\f*$') + 1, 990 | \ }, 991 | \ }, 992 | \ })call setcmdline('') 993 | < 994 | *ddu-ui-ff-faq-14* 995 | Q: I want to switch sources without clear current filter input. 996 | 997 | A: >vim 998 | nnoremap ff 999 | \ call ddu#ui#do_action('updateOptions', #{ 1000 | \ sources: [ 1001 | \ #{ name: 'file' }, 1002 | \ ], 1003 | \ }) 1004 | < 1005 | *ddu-ui-ff-faq-15* 1006 | Q: I want to use existing buffer to show preview. 1007 | 1008 | A: Please use "no" in |ddu-ui-ff-param-previewSplit|. 1009 | 1010 | *ddu-ui-ff-faq-16* 1011 | Q: ":UniteNext"/":UnitePrevious"/":Denite -resume -cursor-pos=+1 -immediately" 1012 | like commands are available? 1013 | 1014 | A: >vim 1015 | nnoremap 1016 | \ call ddu#ui#multi_actions( 1017 | \ ['cursorNext', 'itemAction'], 'files') 1018 | nnoremap 1019 | \ call ddu#ui#multi_actions( 1020 | \ ['cursorPrevious', 'itemAction'], 'files') 1021 | < 1022 | *ddu-ui-ff-faq-17* 1023 | Q: I want to display cursor mark on the left of ddu window like 1024 | "telescope.nvim". 1025 | 1026 | A: >vim 1027 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 1028 | function s:ddu_ff_my_settings() abort 1029 | setlocal signcolumn=yes 1030 | autocmd CursorMoved call s:update_cursor() 1031 | endfunction 1032 | 1033 | function s:update_cursor() 1034 | call sign_unplace('*', #{ 1035 | \ id: 100, 1036 | \ }) 1037 | call sign_define('cursor', #{ 1038 | \ text: '>>', 1039 | \ texthl: 'Search', 1040 | \ }) 1041 | call sign_place('*', #{ 1042 | \ name: 'cursor', 1043 | \ line: '.'->line(), 1044 | \ buffer: '%'->bufnr(), 1045 | \ id: 100, 1046 | \ }) 1047 | endfunction 1048 | < 1049 | *ddu-ui-ff-faq-18* 1050 | Q: How do I set the floating title with list style ? 1051 | 1052 | A: >vim 1053 | call ddu#custom#patch_global(#{ 1054 | \ uiParams: #{ 1055 | \ ff: #{ 1056 | \ floatingBorder: 'rounded', 1057 | \ floatingTitle: [['ddu', 'Red'], ['title', 'Blue']], 1058 | \ } 1059 | \ }, 1060 | \ }) 1061 | < 1062 | *ddu-ui-ff-faq-19* 1063 | Q: When calling |ddu-ui-ff-action-quit| or |ddu-ui-ff-action-itemAction| from 1064 | filter window, it remains filter window. 1065 | 1066 | A: It is the feature. Because |ddu#ui#do_action()| does not change any mode. 1067 | You must escape command line mode when leave from the filter window. 1068 | >vim 1069 | cnoremap 1070 | \ call ddu#ui#do_action('quit') 1071 | cnoremap 1072 | \ call ddu#ui#do_action('itemAction') 1073 | < 1074 | *ddu-ui-ff-faq-20* 1075 | Q: I cannot see what item is selected in the filter window. 1076 | 1077 | A: You can set 'cursorline' option in ddu buffer. 1078 | >vim 1079 | autocmd FileType ddu-ff call s:ddu_ff_my_settings() 1080 | function s:ddu_ff_my_settings() abort 1081 | setlocal cursorline 1082 | endfunction 1083 | < 1084 | *ddu-ui-ff-faq-21* 1085 | Q: I want to change preview cursor on the top. 1086 | 1087 | A: >vim 1088 | call ddu#custom#patch_global(#{ 1089 | \ uiParams: #{ 1090 | \ ff: #{ 1091 | \ onPreview: denops#callback#register( 1092 | \ { args -> execute('normal! zt') } 1093 | \ ), 1094 | \ }, 1095 | \ }, 1096 | \ }) 1097 | < 1098 | *ddu-ui-ff-faq-22* 1099 | Q: I want to scroll preview window. 1100 | 1101 | A: >vim 1102 | nnoremap 1103 | \ call ddu#ui#do_action('previewExecute', 1104 | \ #{ command: 'execute "normal! \"' }) 1105 | nnoremap 1106 | \ call ddu#ui#do_action('previewExecute', 1107 | \ #{ command: 'execute "normal! \"' }) 1108 | < 1109 | *ddu-ui-ff-faq-23* 1110 | Q: I want to preview selected item automatically. 1111 | 1112 | A: You need to set |ddu-ui-ff-param-autoAction| like this. >vim 1113 | call ddu#custom#patch_global(#{ 1114 | \ ui: 'ff', 1115 | \ uiParams: #{ 1116 | \ ff: #{ 1117 | \ autoAction: #{ 1118 | \ name: 'preview', 1119 | \ }, 1120 | \ }, 1121 | \ }, 1122 | \ }) 1123 | < 1124 | *ddu-ui-ff-faq-24* 1125 | Q: I want to start filter window when UI is initialized. 1126 | 1127 | A: Unlike other fuzzy finders UI, it is not supported in ddu-ui-ff. Because 1128 | it has too many asynchronous problems. It is unstable. But you can use the 1129 | function. 1130 | >vim 1131 | call ddu#start() 1132 | autocmd User Ddu:uiDone ++nested 1133 | \ call ddu#ui#async_action('openFilterWindow') 1134 | < 1135 | NOTE: It must be |ddu#ui#async_action()|. Because, in 1136 | |ddu-autocmd-Ddu:uiDone| autocmd, the redraw is locked. 1137 | 1138 | *ddu-ui-ff-faq-25* 1139 | Q: I want to change filter window position. 1140 | 1141 | A: "cmdline.vim" supports the feature. 1142 | >vim 1143 | autocmd User Ddu:uiOpenFilterWindow call cmdline#enable() 1144 | < 1145 | *ddu-ui-ff-faq-26* 1146 | Q: The filter input is cleared when I press key. 1147 | 1148 | A: It is Vim's feature. is not confirm key. It is cancel your input. 1149 | If you don't like the behavior, you can map it. 1150 | 1151 | *ddu-ui-ff-faq-27* 1152 | Q: The cursor is not restored when the items are refreshed. 1153 | 1154 | A: It is feature. The items are refreshed and it will be empty temporary. 1155 | The cursor cannot restore when the items are empty. 1156 | You can prevent the behavior by |ddu-option-sync|. 1157 | 1158 | ============================================================================== 1159 | COMPATIBILITY *ddu-ui-ff-compatibility* 1160 | 1161 | 2025.12.02 1162 | * Rename "statusline" param to "overwriteStatusline". 1163 | 1164 | 2025.02.15 1165 | * Move filter window feature into the ddu core. 1166 | * Remove filter configuration from params. 1167 | 1168 | 2024.05.28 1169 | * Remove |ddu-ui-ff-param-split| floating window support in Vim. Because it 1170 | does not work. 1171 | 1172 | 2024.05.27 1173 | * |ddu-ui-ff-param-cursorPos| is 1 origin. 1174 | 1175 | 2024.05.26 1176 | * Remove "cmdline.vim" support. You can call |cmdline#enable()| in 1177 | |ddu-ui-autocmd-Ddu:uiOpenFilterWindow| autocmd. 1178 | 1179 | 2024.05.22 1180 | * Remove "filterUpdateTime" param. 1181 | * Remove "ddu#ui#ff#execute()". 1182 | 1183 | 2024.05.21 1184 | * Use command line instead of filter window. 1185 | * Remove filter related params: 1186 | "filterFloatingPosition", "filterFloatingTitle", 1187 | "filterFloatingTitlePos", "filterSplitDirection" 1188 | * Remove filter related actions: 1189 | "closeFilterWindow", "leaveFilterWindow" 1190 | * Remove "ddu-ff-filter" filetype. 1191 | Please use "Ddu:ui:ff:openFilterWindow" autocmd instead. 1192 | 1193 | 2024.03.11 1194 | * Remove "startFilter" option. Because it has too many problems. 1195 | 1196 | 2024.01.19 1197 | * Rename "refreshItems" action to "redraw". 1198 | * "updateOptions" action does not redraw automatically. 1199 | 1200 | 2023.07.15 1201 | * Only highlight items of "maxHighlightItems" to improve the performance. 1202 | 1203 | 2023.07.12 1204 | * "preview" action does not toggle. 1205 | 1206 | 2023.07.05 1207 | * "startAutoAction" is required if you want to enable autoAction when UI 1208 | initialized. 1209 | 1210 | 2023.06.11 1211 | * Does not restore to normal mode automatically. It should be configured by 1212 | user. 1213 | 1214 | 2023.05.20 1215 | * 'signcolumn' option does not be set automatically. 1216 | 1217 | 2023.03.19 1218 | * Remove "ddu#ui#ff#get_item()". Please use "ddu#ui#get_item()" instead. 1219 | * Remove "ddu#ui#ff#close()". Please use "closeFilterWindow" action instead. 1220 | 1221 | 2023.02.15 1222 | * Remove "previewVertical". Please use "previewSplit" instead. 1223 | 1224 | 2022.12.28 1225 | * Remove 'cursorline' set. 1226 | 1227 | 2022.11.18 1228 | * Require Neovim 0.8. 1229 | 1230 | 2022.09.06 1231 | * "ddu#ui#ff#execute()" does not execute ":redraw" automatically. 1232 | 1233 | 2022.02.07 1234 | * Rename to "ff". 1235 | 1236 | 2022.02.01 1237 | * Rename "filterPosition" to "filterFloatingPosition". 1238 | 1239 | 2022.01.29 1240 | * Rename "ddu#ui#ff#do_map()" to "ddu#ui#ff#do_action()". 1241 | 1242 | ============================================================================== 1243 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 1244 | -------------------------------------------------------------------------------- /denops/@ddu-uis/ff/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionFlags, 3 | type BaseParams, 4 | type Context, 5 | type DduItem, 6 | type DduOptions, 7 | type PreviewContext, 8 | type Previewer, 9 | type UiOptions, 10 | } from "@shougo/ddu-vim/types"; 11 | import { BaseUi, type UiActions } from "@shougo/ddu-vim/ui"; 12 | import { convertTreePath, printError } from "@shougo/ddu-vim/utils"; 13 | 14 | import type { Denops } from "@denops/std"; 15 | import { batch } from "@denops/std/batch"; 16 | import * as op from "@denops/std/option"; 17 | import * as fn from "@denops/std/function"; 18 | import * as vars from "@denops/std/variable"; 19 | 20 | import { equal } from "@std/assert"; 21 | import { is } from "@core/unknownutil/is"; 22 | import { SEPARATOR as pathsep } from "@std/path/constants"; 23 | import { ensure } from "@denops/std/buffer"; 24 | 25 | import { PreviewUi } from "./preview.ts"; 26 | 27 | type HighlightGroup = { 28 | filterText?: string; 29 | floating?: string; 30 | floatingBorder?: string; 31 | floatingCursorLine?: string; 32 | preview?: string; 33 | selected?: string; 34 | }; 35 | 36 | type AutoAction = { 37 | name?: string; 38 | params?: unknown; 39 | delay?: number; 40 | sync?: boolean; 41 | }; 42 | 43 | type FloatingOpts = { 44 | relative: "editor" | "win" | "cursor" | "mouse"; 45 | row: number; 46 | col: number; 47 | width: number; 48 | height: number; 49 | border?: FloatingBorder; 50 | title?: FloatingTitle; 51 | title_pos?: "left" | "center" | "right"; 52 | }; 53 | 54 | type FloatingBorder = 55 | | "none" 56 | | "single" 57 | | "double" 58 | | "rounded" 59 | | "solid" 60 | | "shadow" 61 | | string[]; 62 | 63 | type FloatingTitleHighlight = string; 64 | 65 | type FloatingTitle = 66 | | string 67 | | [string, FloatingTitleHighlight][]; 68 | 69 | type WindowOption = [string, number | string]; 70 | 71 | type CursorPos = [] | [lnum: number, col: number, off?: number]; 72 | 73 | type ExprNumber = string | number; 74 | 75 | type WinInfo = { 76 | columns: number; 77 | lines: number; 78 | winid: number; 79 | tabpagebuflist: number[]; 80 | }; 81 | 82 | type DoActionParams = { 83 | name?: string; 84 | items?: DduItem[]; 85 | params?: unknown; 86 | }; 87 | 88 | type CursorActionParams = { 89 | count?: number; 90 | loop?: boolean; 91 | }; 92 | 93 | type ExpandItemParams = { 94 | mode?: "toggle"; 95 | maxLevel?: number; 96 | isGrouped?: boolean; 97 | isInTree?: boolean; 98 | }; 99 | 100 | type OpenFilterWindowParams = { 101 | input?: string; 102 | }; 103 | 104 | type OnPreviewArguments = { 105 | denops: Denops; 106 | context: Context; 107 | item: DduItem; 108 | previewContext: PreviewContext; 109 | previewWinId: number; 110 | }; 111 | 112 | type PreviewExecuteParams = { 113 | command: string; 114 | }; 115 | 116 | type RedrawParams = { 117 | method?: "refreshItems" | "uiRedraw" | "uiRefresh"; 118 | }; 119 | 120 | type QuitParams = { 121 | force?: boolean; 122 | }; 123 | 124 | export type Params = { 125 | autoAction: AutoAction; 126 | autoResize: boolean; 127 | cursorPos: number; 128 | displaySourceName: "long" | "short" | "no"; 129 | displayTree: boolean; 130 | exprParams: (keyof Params)[]; 131 | floatingBorder: FloatingBorder; 132 | floatingTitle: FloatingTitle; 133 | floatingTitlePos: "left" | "center" | "right"; 134 | focus: boolean; 135 | highlights: HighlightGroup; 136 | ignoreEmpty: boolean; 137 | immediateAction: string; 138 | maxDisplayItems: number; 139 | maxHighlightItems: number; 140 | maxWidth: number; 141 | onPreview: string | ((args: OnPreviewArguments) => Promise); 142 | overwriteStatusline: boolean; 143 | overwriteTitle: boolean; 144 | pathFilter: string; 145 | previewCol: ExprNumber; 146 | previewFocusable: boolean; 147 | previewFloating: boolean; 148 | previewFloatingBorder: FloatingBorder; 149 | previewFloatingTitle: FloatingTitle; 150 | previewFloatingTitlePos: "left" | "center" | "right"; 151 | previewFloatingZindex: number; 152 | previewHeight: ExprNumber; 153 | previewMaxSize: number; 154 | previewRow: ExprNumber; 155 | previewSplit: "horizontal" | "vertical" | "no"; 156 | previewWidth: ExprNumber; 157 | previewWindowOptions: WindowOption[]; 158 | replaceCol: number; 159 | reversed: boolean; 160 | split: "horizontal" | "vertical" | "floating" | "tab" | "no"; 161 | splitDirection: "belowright" | "aboveleft" | "topleft" | "botright"; 162 | startAutoAction: boolean; 163 | winCol: ExprNumber; 164 | winHeight: ExprNumber; 165 | winRow: ExprNumber; 166 | winWidth: ExprNumber; 167 | }; 168 | 169 | export class Ui extends BaseUi { 170 | #bufferName = ""; 171 | #items: DduItem[] = []; 172 | #viewItems: DduItem[] = []; 173 | #selectedItems: ObjectSet = new ObjectSet(); 174 | #saveMode = ""; 175 | #saveCmdline = ""; 176 | #saveCmdpos = 0; 177 | #saveCol = 0; 178 | #refreshed = false; 179 | #prevLength = -1; 180 | #prevInput = ""; 181 | #prevWinInfo: WinInfo | null = null; 182 | #previewUi = new PreviewUi(); 183 | #popupId = -1; 184 | #enabledAutoAction = false; 185 | #restcmd = ""; 186 | #closing = false; 187 | 188 | override async onInit(args: { 189 | denops: Denops; 190 | uiParams: Params; 191 | }): Promise { 192 | this.#saveMode = await fn.mode(args.denops); 193 | if (this.#saveMode === "c") { 194 | this.#saveMode = await fn.getcmdtype(args.denops) as string; 195 | if (this.#saveMode === ":") { 196 | // Save command line state 197 | this.#saveCmdline = await fn.getcmdline(args.denops) as string; 198 | this.#saveCmdpos = await fn.getcmdpos(args.denops) as number; 199 | } 200 | } else { 201 | this.#saveCol = await fn.col(args.denops, ".") as number; 202 | } 203 | 204 | this.#items = []; 205 | await this.clearSelectedItems(args); 206 | 207 | this.#enabledAutoAction = args.uiParams.startAutoAction; 208 | 209 | await this.#clearSavedCursor(args.denops); 210 | } 211 | 212 | override async onBeforeAction(args: { 213 | denops: Denops; 214 | }): Promise { 215 | await vars.g.set(args.denops, "ddu#ui#ff#_in_action", true); 216 | 217 | const bufnr = await fn.bufnr(args.denops, this.#bufferName); 218 | if (await fn.bufnr(args.denops, "%") === bufnr) { 219 | await vars.b.set( 220 | args.denops, 221 | "ddu_ui_ff_cursor_pos", 222 | await fn.getcurpos(args.denops), 223 | ); 224 | await vars.b.set( 225 | args.denops, 226 | "ddu_ui_ff_cursor_text", 227 | await fn.getline(args.denops, "."), 228 | ); 229 | } 230 | } 231 | 232 | override async onAfterAction(args: { 233 | denops: Denops; 234 | }): Promise { 235 | await vars.g.set(args.denops, "ddu#ui#ff#_in_action", false); 236 | } 237 | 238 | override async refreshItems(args: { 239 | denops: Denops; 240 | context: Context; 241 | uiParams: Params; 242 | items: DduItem[]; 243 | }): Promise { 244 | this.#prevLength = this.#items.length; 245 | this.#prevInput = args.context.input; 246 | 247 | this.#items = args.items.slice(0, args.uiParams.maxDisplayItems); 248 | if (args.uiParams.pathFilter !== "") { 249 | const pathFilter = new RegExp(args.uiParams.pathFilter); 250 | type ActionPath = { 251 | path: string; 252 | }; 253 | this.#items = this.#items.filter((item) => 254 | (item?.action as ActionPath)?.path && 255 | pathFilter.test((item?.action as ActionPath)?.path) 256 | ); 257 | } 258 | 259 | await this.#updateSelectedItems(args.denops); 260 | 261 | this.#refreshed = true; 262 | 263 | await this.#clearSavedCursor(args.denops); 264 | 265 | return Promise.resolve(); 266 | } 267 | 268 | override async searchItem(args: { 269 | denops: Denops; 270 | context: Context; 271 | item: DduItem; 272 | uiParams: Params; 273 | }) { 274 | const bufnr = await this.#getBufnr(args.denops); 275 | if (bufnr !== await fn.bufnr(args.denops)) { 276 | return; 277 | } 278 | 279 | let index = this.#items.findIndex( 280 | (item) => equal(item, args.item), 281 | ); 282 | if (index < 0) { 283 | // NOTE: Use treePath to search item. Because item state may be changed. 284 | const itemTreePath = convertTreePath( 285 | args.item.treePath ?? args.item.word, 286 | ); 287 | index = this.#items.findIndex( 288 | (item) => 289 | equal(convertTreePath(item.treePath ?? item.word), itemTreePath), 290 | ); 291 | } 292 | 293 | if (index < 0) { 294 | return; 295 | } 296 | 297 | // NOTE: cursorPos is not same with item pos when reversed. 298 | const cursorPos = args.uiParams.reversed 299 | ? this.#items.length - index 300 | : index + 1; 301 | 302 | const winHeight = await fn.winheight(args.denops, 0); 303 | const maxLine = await fn.line(args.denops, "$"); 304 | if ((maxLine - cursorPos) < winHeight / 2) { 305 | // Adjust cursor position when cursor is near bottom. 306 | await args.denops.cmd("normal! Gzb"); 307 | } 308 | await this.#cursor(args.denops, args.context, [cursorPos, 0]); 309 | if (cursorPos < winHeight / 2) { 310 | // Adjust cursor position when cursor is near top. 311 | await args.denops.cmd("normal! zb"); 312 | } 313 | 314 | await args.denops.call("ddu#ui#ff#_update_cursor"); 315 | } 316 | 317 | override async redraw(args: { 318 | denops: Denops; 319 | context: Context; 320 | options: DduOptions; 321 | uiOptions: UiOptions; 322 | uiParams: Params; 323 | }): Promise { 324 | if (args.options.sync && !args.context.done) { 325 | // Skip redraw if all items are not done 326 | return; 327 | } 328 | 329 | if (args.context.done && this.#items.length === 0) { 330 | // Close preview window when empty items 331 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 332 | } 333 | 334 | this.#bufferName = `ddu-ff-${args.options.name}`; 335 | 336 | const existsUI = await this.#winId({ 337 | denops: args.denops, 338 | uiParams: args.uiParams, 339 | }) > 0; 340 | 341 | if (!existsUI) { 342 | if (args.uiParams.ignoreEmpty && this.#items.length === 0) { 343 | // Disable open UI window when empty items 344 | return; 345 | } 346 | 347 | if ( 348 | args.uiParams.immediateAction.length != 0 && 349 | this.#items.length === 1 350 | ) { 351 | // Immediate action 352 | await args.denops.call( 353 | "ddu#item_action", 354 | args.options.name, 355 | args.uiParams.immediateAction, 356 | this.#items, 357 | {}, 358 | ); 359 | return; 360 | } 361 | } 362 | 363 | const initialized = await fn.bufexists(args.denops, this.#bufferName) && 364 | await fn.bufnr(args.denops, this.#bufferName); 365 | 366 | const bufnr = initialized || 367 | await initBuffer(args.denops, this.#bufferName); 368 | 369 | const augroupName = `ddu-ui-ff-${bufnr}`; 370 | await args.denops.cmd(`augroup ${augroupName}`); 371 | await args.denops.cmd(`autocmd! ${augroupName}`); 372 | 373 | args.uiParams = await this.#resolveParams( 374 | args.denops, 375 | args.options, 376 | args.uiParams, 377 | args.context, 378 | ); 379 | 380 | const hasNvim = args.denops.meta.host === "nvim"; 381 | //const floating = args.uiParams.split === "floating"; 382 | const floating = args.uiParams.split === "floating" && hasNvim; 383 | const winWidth = Number(args.uiParams.winWidth); 384 | let winHeight = args.uiParams.autoResize && 385 | this.#items.length < Number(args.uiParams.winHeight) 386 | ? Math.max(this.#items.length, 1) 387 | : Number(args.uiParams.winHeight); 388 | const prevWinid = await this.#winId({ 389 | denops: args.denops, 390 | uiParams: args.uiParams, 391 | }); 392 | 393 | if (prevWinid < 0) { 394 | // The layout must be saved. 395 | this.#restcmd = await fn.winrestcmd(args.denops); 396 | this.#prevWinInfo = await getWinInfo(args.denops); 397 | } 398 | 399 | const direction = args.uiParams.splitDirection; 400 | if (args.uiParams.split === "horizontal") { 401 | // NOTE: If winHeight is bigger than `&lines / 2`, it will be resized. 402 | const maxWinHeight = Math.floor( 403 | await op.lines.getGlobal(args.denops) * 4 / 10, 404 | ); 405 | if (winHeight > maxWinHeight) { 406 | winHeight = maxWinHeight; 407 | } 408 | 409 | if (prevWinid >= 0) { 410 | await fn.win_execute( 411 | args.denops, 412 | prevWinid, 413 | `resize ${winHeight}`, 414 | ); 415 | } else { 416 | const header = `silent keepalt ${direction} `; 417 | await args.denops.cmd( 418 | header + `sbuffer +resize\\ ${winHeight} ${bufnr}`, 419 | ); 420 | } 421 | } else if (args.uiParams.split === "vertical") { 422 | if (prevWinid >= 0) { 423 | await fn.win_execute( 424 | args.denops, 425 | prevWinid, 426 | `vertical resize ${winWidth}`, 427 | ); 428 | } else { 429 | const header = `silent keepalt vertical ${direction} `; 430 | await args.denops.cmd( 431 | header + 432 | `sbuffer +vertical\\ resize\\ ${winWidth} ${bufnr}`, 433 | ); 434 | } 435 | } else if (floating) { 436 | // statusline must be set for floating window 437 | const currentStatusline = await op.statusline.getLocal(args.denops); 438 | const floatingHighlight = args.uiParams.highlights?.floating ?? 439 | "NormalFloat"; 440 | const borderHighlight = args.uiParams.highlights?.floatingBorder ?? 441 | "FloatBorder"; 442 | const cursorLineHighlight = 443 | args.uiParams.highlights?.floatingCursorLine ?? "CursorLine"; 444 | 445 | if (hasNvim) { 446 | const winOpts: FloatingOpts = { 447 | "relative": "editor", 448 | "row": Number(args.uiParams.winRow), 449 | "col": Number(args.uiParams.winCol), 450 | "width": winWidth, 451 | "height": winHeight, 452 | "border": args.uiParams.floatingBorder, 453 | "title": args.uiParams.floatingTitle, 454 | "title_pos": args.uiParams.floatingTitlePos, 455 | }; 456 | if ( 457 | this.#popupId >= 0 && 458 | await fn.bufwinid(args.denops, bufnr) === this.#popupId 459 | ) { 460 | try { 461 | await args.denops.call( 462 | "nvim_win_set_config", 463 | this.#popupId, 464 | winOpts, 465 | ); 466 | } catch (_) { 467 | // The window may be closed. 468 | } 469 | } else { 470 | this.#popupId = await args.denops.call( 471 | "nvim_open_win", 472 | bufnr, 473 | true, 474 | winOpts, 475 | ) as number; 476 | } 477 | } else { 478 | const winOpts = { 479 | "pos": "topleft", 480 | "line": Number(args.uiParams.winRow) + 1, 481 | "col": Number(args.uiParams.winCol) + 1, 482 | "highlight": floatingHighlight, 483 | "border": [], 484 | "borderchars": [], 485 | "borderhighlight": [borderHighlight], 486 | "minwidth": Number(args.uiParams.winWidth), 487 | "maxwidth": Number(args.uiParams.winWidth), 488 | "minheight": winHeight, 489 | "maxheight": winHeight, 490 | "scrollbar": 0, 491 | "title": args.uiParams.floatingTitle, 492 | "wrap": 0, 493 | "focusable": 1, 494 | } as Record; 495 | 496 | switch (args.uiParams.floatingBorder) { 497 | case "none": 498 | break; 499 | case "single": 500 | case "rounded": 501 | case "solid": 502 | case "shadow": 503 | winOpts["border"] = [1, 1, 1, 1]; 504 | break; 505 | case "double": 506 | winOpts["border"] = [2, 2, 2, 2]; 507 | break; 508 | default: 509 | winOpts["borderchars"] = args.uiParams.floatingBorder; 510 | } 511 | if ( 512 | this.#popupId >= 0 && 513 | await fn.bufwinid(args.denops, bufnr) === this.#popupId 514 | ) { 515 | await args.denops.call( 516 | "popup_move", 517 | this.#popupId, 518 | winOpts, 519 | ); 520 | } else { 521 | this.#popupId = await args.denops.call( 522 | "popup_create", 523 | bufnr, 524 | winOpts, 525 | ) as number; 526 | } 527 | } 528 | 529 | if (hasNvim) { 530 | await fn.setwinvar( 531 | args.denops, 532 | this.#popupId, 533 | "&winhighlight", 534 | `Normal:${floatingHighlight},FloatBorder:${borderHighlight},` + 535 | `CursorLine:${cursorLineHighlight}`, 536 | ); 537 | 538 | await fn.setwinvar( 539 | args.denops, 540 | this.#popupId, 541 | "&statusline", 542 | currentStatusline, 543 | ); 544 | } else { 545 | await fn.setwinvar( 546 | args.denops, 547 | this.#popupId, 548 | "&cursorline", 549 | true, 550 | ); 551 | 552 | if (cursorLineHighlight !== "CursorLine") { 553 | await fn.win_execute( 554 | args.denops, 555 | this.#popupId, 556 | `highlight! link CursorLine ${cursorLineHighlight}`, 557 | ); 558 | } 559 | } 560 | } else if (args.uiParams.split === "tab") { 561 | if (prevWinid >= 0) { 562 | await fn.win_gotoid(args.denops, prevWinid); 563 | } else { 564 | // NOTE: ":tabnew" creates new empty buffer. 565 | await args.denops.cmd(`silent keepalt tab sbuffer ${bufnr}`); 566 | } 567 | } else if (args.uiParams.split === "no") { 568 | if (prevWinid < 0) { 569 | await args.denops.cmd(`silent keepalt buffer ${bufnr}`); 570 | } 571 | } else { 572 | await printError( 573 | args.denops, 574 | `Invalid split param: ${args.uiParams.split}`, 575 | ); 576 | return; 577 | } 578 | 579 | const winid = await this.#winId({ 580 | denops: args.denops, 581 | uiParams: args.uiParams, 582 | }); 583 | 584 | await this.#setAutoAction(args.denops, args.uiParams, winid); 585 | 586 | const prevWinnr = await fn.winnr(args.denops, "#"); 587 | if ( 588 | args.uiParams.autoResize && prevWinnr > 0 && 589 | prevWinnr !== await fn.winnr(args.denops) 590 | ) { 591 | await fn.win_execute( 592 | args.denops, 593 | winid, 594 | `resize ${winHeight} | normal! zb`, 595 | ); 596 | } 597 | 598 | if (!initialized || winid < 0) { 599 | await this.#initOptions(args.denops, args.options, args.uiParams, bufnr); 600 | } 601 | if (!initialized) { 602 | // Update cursor when cursor moved 603 | await args.denops.cmd( 604 | "autocmd CursorMoved call ddu#ui#ff#_update_cursor()", 605 | ); 606 | } 607 | 608 | await setStatusline( 609 | args.denops, 610 | args.context, 611 | args.options, 612 | args.uiParams, 613 | await this.#winId({ 614 | denops: args.denops, 615 | uiParams: args.uiParams, 616 | }), 617 | augroupName, 618 | this.#items, 619 | ); 620 | 621 | // Update main buffer 622 | const displaySourceName = args.uiParams.displaySourceName; 623 | const getSourceName = (sourceName: string) => { 624 | if (displaySourceName === "long") { 625 | return sourceName + " "; 626 | } 627 | if (displaySourceName === "short") { 628 | return sourceName.match(/[^a-zA-Z]/) 629 | ? sourceName.replaceAll(/([a-zA-Z])[a-zA-Z]+/g, "$1") + " " 630 | : sourceName.slice(0, 2) + " "; 631 | } 632 | return ""; 633 | }; 634 | const cursorPos = Number(args.uiParams.cursorPos) > 0 && this.#refreshed && 635 | this.#prevLength == 0 636 | ? Number(args.uiParams.cursorPos) 637 | : 0; 638 | 639 | const getPrefix = (item: DduItem) => { 640 | return `${getSourceName(item.__sourceName)}` + 641 | (args.uiParams.displayTree 642 | ? " ".repeat(item.__level) + 643 | (!item.isTree ? " " : item.__expanded ? "- " : "+ ") 644 | : ""); 645 | }; 646 | 647 | // Update main buffer 648 | try { 649 | const checkRefreshed = args.context.input !== this.#prevInput || 650 | (this.#prevLength > 0 && this.#items.length < this.#prevLength) || 651 | (args.uiParams.reversed && this.#items.length !== this.#prevLength); 652 | // NOTE: Use batch for screen flicker when highlight items. 653 | await batch(args.denops, async (denops: Denops) => { 654 | await ensure(args.denops, bufnr, async () => { 655 | await denops.call( 656 | "ddu#ui#ff#_update_buffer", 657 | args.uiParams, 658 | bufnr, 659 | winid, 660 | this.#items.map((c) => getPrefix(c) + (c.display ?? c.word)), 661 | args.uiParams.cursorPos > 0 || (this.#refreshed && checkRefreshed), 662 | cursorPos, 663 | ); 664 | await denops.call( 665 | "ddu#ui#ff#_process_items", 666 | args.uiParams, 667 | bufnr, 668 | this.#items.length, 669 | this.#items.map((item, index) => { 670 | return { 671 | highlights: item.highlights ?? [], 672 | info: item.info ?? [], 673 | row: index + 1, 674 | prefix: getPrefix(item), 675 | }; 676 | }).slice(0, args.uiParams.maxHighlightItems), 677 | this.#selectedItems.values() 678 | .map((item) => this.#getItemIndex(item)) 679 | .filter((index) => index >= 0), 680 | ); 681 | }); 682 | }); 683 | } catch (e) { 684 | await printError( 685 | args.denops, 686 | e, 687 | "[ddu-ui-ff] update buffer failed", 688 | ); 689 | return; 690 | } 691 | 692 | this.#viewItems = Array.from(this.#items); 693 | if (args.uiParams.reversed) { 694 | this.#viewItems = this.#viewItems.reverse(); 695 | } 696 | 697 | const saveItem = await fn.getbufvar( 698 | args.denops, 699 | bufnr, 700 | "ddu_ui_item", 701 | {}, 702 | ) as DduItem; 703 | 704 | if (cursorPos <= 0 && Object.keys(saveItem).length !== 0) { 705 | this.searchItem({ 706 | denops: args.denops, 707 | context: args.context, 708 | item: saveItem, 709 | uiParams: args.uiParams, 710 | }); 711 | } 712 | 713 | if (!initialized || cursorPos > 0) { 714 | // Update current cursor 715 | await this.updateCursor({ denops: args.denops, context: args.context }); 716 | } 717 | 718 | await this.#doAutoAction(args.denops); 719 | 720 | await fn.setbufvar(args.denops, bufnr, "ddu_ui_items", this.#items); 721 | 722 | if (!existsUI) { 723 | await fn.win_gotoid( 724 | args.denops, 725 | args.uiParams.focus ? winid : args.context.winId, 726 | ); 727 | } 728 | 729 | this.#refreshed = false; 730 | } 731 | 732 | override async clearSelectedItems(args: { 733 | denops: Denops; 734 | }) { 735 | this.#selectedItems.clear(); 736 | const bufnr = await this.#getBufnr(args.denops); 737 | await fn.setbufvar(args.denops, bufnr, "ddu_ui_selected_items", []); 738 | } 739 | 740 | override async quit(args: { 741 | denops: Denops; 742 | context: Context; 743 | options: DduOptions; 744 | uiParams: Params; 745 | }): Promise { 746 | await this.#close({ 747 | denops: args.denops, 748 | context: args.context, 749 | options: args.options, 750 | uiParams: args.uiParams, 751 | cancel: false, 752 | }); 753 | } 754 | 755 | override async expandItem(args: { 756 | denops: Denops; 757 | uiParams: Params; 758 | parent: DduItem; 759 | children: DduItem[]; 760 | isGrouped: boolean; 761 | }): Promise { 762 | // NOTE: treePath may be list. So it must be compared by JSON. 763 | const index = this.#items.findIndex( 764 | (item: DduItem) => 765 | equal(item.treePath, args.parent.treePath) && 766 | item.__sourceIndex === args.parent.__sourceIndex, 767 | ); 768 | 769 | const insertItems = args.children; 770 | 771 | const prevLength = this.#items.length; 772 | if (index >= 0) { 773 | if (args.isGrouped) { 774 | // Replace parent 775 | this.#items[index] = insertItems[0]; 776 | } else { 777 | this.#items = [ 778 | ...this.#items.slice(0, index + 1), 779 | ...insertItems, 780 | ...this.#items.slice(index + 1), 781 | ]; 782 | this.#items[index] = args.parent; 783 | } 784 | } else { 785 | this.#items = [...this.#items, ...insertItems]; 786 | } 787 | 788 | await this.#updateSelectedItems(args.denops); 789 | 790 | return Promise.resolve(prevLength - this.#items.length); 791 | } 792 | 793 | override async collapseItem(args: { 794 | denops: Denops; 795 | item: DduItem; 796 | }): Promise { 797 | // NOTE: treePath may be list. So it must be compared by JSON. 798 | const startIndex = this.#items.findIndex( 799 | (item: DduItem) => 800 | equal(item.treePath, args.item.treePath) && 801 | item.__sourceIndex === args.item.__sourceIndex, 802 | ); 803 | if (startIndex < 0) { 804 | return Promise.resolve(0); 805 | } 806 | 807 | const endIndex = this.#items.slice(startIndex + 1).findIndex( 808 | (item: DduItem) => item.__level <= args.item.__level, 809 | ); 810 | 811 | const prevLength = this.#items.length; 812 | if (endIndex < 0) { 813 | this.#items = this.#items.slice(0, startIndex + 1); 814 | } else { 815 | this.#items = [ 816 | ...this.#items.slice(0, startIndex + 1), 817 | ...this.#items.slice(startIndex + endIndex + 1), 818 | ]; 819 | } 820 | 821 | this.#items[startIndex] = args.item; 822 | 823 | await this.#updateSelectedItems(args.denops); 824 | 825 | return Promise.resolve(prevLength - this.#items.length); 826 | } 827 | 828 | override async visible(args: { 829 | denops: Denops; 830 | context: Context; 831 | options: DduOptions; 832 | uiParams: Params; 833 | tabNr: number; 834 | }): Promise { 835 | // NOTE: Vim's floating window cannot find from buffer list. 836 | if (this.#popupId > 0) { 837 | return true; 838 | } 839 | 840 | const bufnr = await this.#getBufnr(args.denops); 841 | if (args.tabNr > 0) { 842 | return (await fn.tabpagebuflist(args.denops, args.tabNr) as number[]) 843 | .includes(bufnr); 844 | } else { 845 | // Search from all tabpages. 846 | return (await fn.win_findbuf(args.denops, bufnr) as number[]).length > 0; 847 | } 848 | } 849 | 850 | async #winId(args: { 851 | denops: Denops; 852 | uiParams: Params; 853 | }): Promise { 854 | // NOTE: In Vim popup window, win_findbuf()/winbufnr() does not work. 855 | if ( 856 | args.uiParams.split === "floating" && 857 | args.denops.meta.host !== "nvim" && this.#popupId > 0 858 | ) { 859 | return this.#popupId; 860 | } 861 | 862 | const bufnr = await this.#getBufnr(args.denops); 863 | const winIds = await fn.win_findbuf(args.denops, bufnr) as number[]; 864 | return winIds.length > 0 ? winIds[0] : -1; 865 | } 866 | 867 | override async winIds(args: { 868 | denops: Denops; 869 | uiParams: Params; 870 | }): Promise { 871 | const winIds = []; 872 | 873 | const mainWinId = await this.#winId(args); 874 | if (mainWinId > 0) { 875 | winIds.push(mainWinId); 876 | } 877 | 878 | if (this.#previewUi.visible()) { 879 | winIds.push(this.#previewUi.previewWinId); 880 | } 881 | 882 | return winIds; 883 | } 884 | 885 | override async updateCursor(args: { 886 | denops: Denops; 887 | context: Context; 888 | }) { 889 | const item = await this.#getItem(args.denops); 890 | const bufnr = await this.#getBufnr(args.denops); 891 | await fn.setbufvar(args.denops, bufnr, "ddu_ui_item", item ?? {}); 892 | } 893 | 894 | override actions: UiActions = { 895 | checkItems: (args: { 896 | denops: Denops; 897 | options: DduOptions; 898 | }) => { 899 | args.denops.dispatcher.redraw(args.options.name, { 900 | check: true, 901 | method: "refreshItems", 902 | }); 903 | 904 | return ActionFlags.None; 905 | }, 906 | chooseAction: async (args: { 907 | denops: Denops; 908 | context: Context; 909 | options: DduOptions; 910 | uiParams: Params; 911 | actionParams: BaseParams; 912 | }) => { 913 | const items = await this.#getItems(args.denops); 914 | 915 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 916 | 917 | await args.denops.dispatcher.start({ 918 | name: args.options.name, 919 | push: true, 920 | sources: [ 921 | { 922 | name: "action", 923 | params: { 924 | name: args.options.name, 925 | items, 926 | }, 927 | }, 928 | ], 929 | }); 930 | 931 | return ActionFlags.None; 932 | }, 933 | chooseInput: async (args: { 934 | denops: Denops; 935 | context: Context; 936 | options: DduOptions; 937 | uiParams: Params; 938 | actionParams: BaseParams; 939 | }) => { 940 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 941 | 942 | await args.denops.dispatcher.start({ 943 | name: args.options.name, 944 | push: true, 945 | sources: [ 946 | { 947 | name: "input_history", 948 | params: { 949 | name: args.options.name, 950 | }, 951 | }, 952 | ], 953 | }); 954 | 955 | return ActionFlags.None; 956 | }, 957 | clearSelectAllItems: async (args: { 958 | denops: Denops; 959 | }) => { 960 | await this.clearSelectedItems(args); 961 | 962 | return Promise.resolve(ActionFlags.Redraw); 963 | }, 964 | collapseItem: async (args: { 965 | denops: Denops; 966 | options: DduOptions; 967 | }) => { 968 | return await this.#collapseItemAction(args.denops, args.options); 969 | }, 970 | closePreviewWindow: async (args: { 971 | denops: Denops; 972 | context: Context; 973 | uiParams: Params; 974 | }) => { 975 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 976 | return ActionFlags.None; 977 | }, 978 | cursorNext: async (args: { 979 | denops: Denops; 980 | context: Context; 981 | options: DduOptions; 982 | uiParams: Params; 983 | actionParams: BaseParams; 984 | }) => { 985 | const bufnr = await this.#getBufnr(args.denops); 986 | const cursorPos = await fn.getbufvar( 987 | args.denops, 988 | bufnr, 989 | "ddu_ui_ff_cursor_pos", 990 | [], 991 | ) as number[]; 992 | if (cursorPos.length === 0) { 993 | return ActionFlags.Persist; 994 | } 995 | 996 | const params = args.actionParams as CursorActionParams; 997 | const count = params.count ?? 1; 998 | const loop = params.loop ?? false; 999 | if (count === 0) { 1000 | return ActionFlags.Persist; 1001 | } 1002 | 1003 | // Move to the next 1004 | if (args.uiParams.reversed) { 1005 | cursorPos[1] -= count; 1006 | } else { 1007 | cursorPos[1] += count; 1008 | } 1009 | if (cursorPos[1] <= 0) { 1010 | cursorPos[1] = loop ? this.#viewItems.length : 1; 1011 | } else if (cursorPos[1] > this.#viewItems.length) { 1012 | cursorPos[1] = loop ? 1 : this.#viewItems.length; 1013 | } 1014 | 1015 | await this.#cursor(args.denops, args.context, [ 1016 | cursorPos[1], 1017 | cursorPos[2], 1018 | ]); 1019 | 1020 | await setStatusline( 1021 | args.denops, 1022 | args.context, 1023 | args.options, 1024 | args.uiParams, 1025 | await this.#winId({ 1026 | denops: args.denops, 1027 | uiParams: args.uiParams, 1028 | }), 1029 | `ddu-ui-ff-${bufnr}`, 1030 | this.#items, 1031 | ); 1032 | 1033 | return ActionFlags.Persist; 1034 | }, 1035 | cursorPrevious: async (args: { 1036 | denops: Denops; 1037 | context: Context; 1038 | options: DduOptions; 1039 | uiParams: Params; 1040 | actionParams: BaseParams; 1041 | }) => { 1042 | const bufnr = await this.#getBufnr(args.denops); 1043 | const cursorPos = await fn.getbufvar( 1044 | args.denops, 1045 | bufnr, 1046 | "ddu_ui_ff_cursor_pos", 1047 | [], 1048 | ) as number[]; 1049 | if (cursorPos.length === 0) { 1050 | return ActionFlags.Persist; 1051 | } 1052 | 1053 | const params = args.actionParams as CursorActionParams; 1054 | const count = params.count ?? 1; 1055 | const loop = params.loop ?? false; 1056 | if (count === 0) { 1057 | return ActionFlags.Persist; 1058 | } 1059 | 1060 | // Move to the previous 1061 | if (args.uiParams.reversed) { 1062 | cursorPos[1] += count; 1063 | } else { 1064 | cursorPos[1] -= count; 1065 | } 1066 | if (cursorPos[1] <= 0) { 1067 | cursorPos[1] = loop ? this.#viewItems.length : 1; 1068 | } else if (cursorPos[1] > this.#viewItems.length) { 1069 | cursorPos[1] = loop ? 1 : this.#viewItems.length; 1070 | } 1071 | 1072 | await this.#cursor(args.denops, args.context, [ 1073 | cursorPos[1], 1074 | cursorPos[2], 1075 | ]); 1076 | 1077 | await setStatusline( 1078 | args.denops, 1079 | args.context, 1080 | args.options, 1081 | args.uiParams, 1082 | await this.#winId({ 1083 | denops: args.denops, 1084 | uiParams: args.uiParams, 1085 | }), 1086 | `ddu-ui-ff-${bufnr}`, 1087 | this.#items, 1088 | ); 1089 | 1090 | return ActionFlags.Persist; 1091 | }, 1092 | cursorTreeBottom: async (args: { 1093 | denops: Denops; 1094 | context: Context; 1095 | uiParams: Params; 1096 | actionParams: BaseParams; 1097 | }) => { 1098 | const bufnr = await this.#getBufnr(args.denops); 1099 | const cursorPos = await fn.getbufvar( 1100 | args.denops, 1101 | bufnr, 1102 | "ddu_ui_ff_cursor_pos", 1103 | [], 1104 | ) as number[]; 1105 | if (cursorPos.length === 0 || !cursorPos[1] || !cursorPos[2]) { 1106 | return ActionFlags.Persist; 1107 | } 1108 | 1109 | // Search tree top 1110 | const item = await this.#getItem(args.denops); 1111 | const targetLevel = item?.__level ?? 0; 1112 | let idx = await this.#getIndex(args.denops); 1113 | let minIndex = idx; 1114 | 1115 | while (idx < this.#viewItems.length) { 1116 | if (this.#viewItems[idx].__level === targetLevel) { 1117 | minIndex = idx; 1118 | } 1119 | if (this.#viewItems[idx].__level < targetLevel) { 1120 | break; 1121 | } 1122 | 1123 | idx++; 1124 | } 1125 | cursorPos[1] = minIndex + 1; 1126 | 1127 | await this.#cursor(args.denops, args.context, [ 1128 | cursorPos[1], 1129 | cursorPos[2], 1130 | ]); 1131 | 1132 | return ActionFlags.Persist; 1133 | }, 1134 | cursorTreeTop: async (args: { 1135 | denops: Denops; 1136 | context: Context; 1137 | uiParams: Params; 1138 | actionParams: BaseParams; 1139 | }) => { 1140 | const bufnr = await this.#getBufnr(args.denops); 1141 | const cursorPos = await fn.getbufvar( 1142 | args.denops, 1143 | bufnr, 1144 | "ddu_ui_ff_cursor_pos", 1145 | [], 1146 | ) as number[]; 1147 | if (cursorPos.length === 0 || !cursorPos[1] || !cursorPos[2]) { 1148 | return ActionFlags.Persist; 1149 | } 1150 | 1151 | // Search tree top 1152 | const item = await this.#getItem(args.denops); 1153 | const targetLevel = item?.__level ?? 0; 1154 | let idx = await this.#getIndex(args.denops); 1155 | let minIndex = idx; 1156 | 1157 | while (idx >= 0) { 1158 | if (this.#viewItems[idx].__level === targetLevel) { 1159 | minIndex = idx; 1160 | } 1161 | if (this.#viewItems[idx].__level < targetLevel) { 1162 | break; 1163 | } 1164 | 1165 | idx--; 1166 | } 1167 | cursorPos[1] = minIndex + 1; 1168 | 1169 | await this.#cursor(args.denops, args.context, [ 1170 | cursorPos[1], 1171 | cursorPos[2], 1172 | ]); 1173 | 1174 | return ActionFlags.Persist; 1175 | }, 1176 | expandItem: async (args: { 1177 | denops: Denops; 1178 | options: DduOptions; 1179 | actionParams: BaseParams; 1180 | }) => { 1181 | const item = await this.#getItem(args.denops); 1182 | if (!item) { 1183 | return ActionFlags.None; 1184 | } 1185 | 1186 | const params = args.actionParams as ExpandItemParams; 1187 | 1188 | if (item.__expanded) { 1189 | if (params.mode === "toggle") { 1190 | return await this.#collapseItemAction(args.denops, args.options); 1191 | } 1192 | return ActionFlags.None; 1193 | } 1194 | 1195 | await args.denops.dispatcher.redrawTree( 1196 | args.options.name, 1197 | "expand", 1198 | [{ 1199 | item, 1200 | maxLevel: params.maxLevel ?? 0, 1201 | isGrouped: params.isGrouped ?? false, 1202 | isInTree: params.isInTree ?? false, 1203 | }], 1204 | ); 1205 | 1206 | return ActionFlags.None; 1207 | }, 1208 | inputAction: async (args: { 1209 | denops: Denops; 1210 | options: DduOptions; 1211 | uiParams: Params; 1212 | }) => { 1213 | const items = await this.#getItems(args.denops); 1214 | 1215 | const actions = await args.denops.dispatcher.getItemActionNames( 1216 | args.options.name, 1217 | items, 1218 | ); 1219 | 1220 | const actionName = await args.denops.call( 1221 | "ddu#util#input_list", 1222 | "Input action name: ", 1223 | actions, 1224 | ); 1225 | if (actionName !== "") { 1226 | await args.denops.call( 1227 | "ddu#item_action", 1228 | args.options.name, 1229 | actionName, 1230 | items, 1231 | {}, 1232 | ); 1233 | } 1234 | 1235 | return ActionFlags.None; 1236 | }, 1237 | itemAction: async (args: { 1238 | denops: Denops; 1239 | options: DduOptions; 1240 | uiParams: Params; 1241 | actionParams: BaseParams; 1242 | }) => { 1243 | const params = args.actionParams as DoActionParams; 1244 | 1245 | const items = params.items ?? await this.#getItems(args.denops); 1246 | if (items.length === 0) { 1247 | return ActionFlags.Persist; 1248 | } 1249 | 1250 | await args.denops.call( 1251 | "ddu#item_action", 1252 | args.options.name, 1253 | params.name ?? "default", 1254 | items, 1255 | params.params ?? {}, 1256 | ); 1257 | 1258 | return ActionFlags.None; 1259 | }, 1260 | openFilterWindow: async (args: { 1261 | denops: Denops; 1262 | context: Context; 1263 | options: DduOptions; 1264 | uiOptions: UiOptions; 1265 | uiParams: Params; 1266 | actionParams: BaseParams; 1267 | getPreviewer?: ( 1268 | denops: Denops, 1269 | item: DduItem, 1270 | actionParams: BaseParams, 1271 | previewContext: PreviewContext, 1272 | ) => Promise; 1273 | inputHistory: string[]; 1274 | }) => { 1275 | const uiParams = await this.#resolveParams( 1276 | args.denops, 1277 | args.options, 1278 | args.uiParams, 1279 | args.context, 1280 | ); 1281 | const reopenPreview = this.#previewUi.visible() && 1282 | uiParams.split === "horizontal" && uiParams.previewSplit === "vertical"; 1283 | 1284 | if (reopenPreview) { 1285 | await this.#previewUi.close(args.denops, args.context, uiParams); 1286 | } 1287 | 1288 | const actionParams = args.actionParams as OpenFilterWindowParams; 1289 | 1290 | args.context.input = await args.denops.call( 1291 | "ddu#ui#_open_filter_window", 1292 | args.uiOptions, 1293 | actionParams.input ?? args.context.input, 1294 | args.options.name, 1295 | this.#items.length, 1296 | args.inputHistory, 1297 | ) as string; 1298 | 1299 | if (reopenPreview) { 1300 | const item = await this.#getItem(args.denops); 1301 | if (!item || !args.getPreviewer) { 1302 | return ActionFlags.None; 1303 | } 1304 | 1305 | return this.#previewUi.previewContents( 1306 | args.denops, 1307 | args.context, 1308 | uiParams, 1309 | args.actionParams, 1310 | await this.#getBufnr(args.denops), 1311 | item, 1312 | args.getPreviewer, 1313 | ); 1314 | } 1315 | 1316 | return ActionFlags.None; 1317 | }, 1318 | preview: async (args: { 1319 | denops: Denops; 1320 | context: Context; 1321 | options: DduOptions; 1322 | uiParams: Params; 1323 | actionParams: BaseParams; 1324 | getPreviewer?: ( 1325 | denops: Denops, 1326 | item: DduItem, 1327 | actionParams: BaseParams, 1328 | previewContext: PreviewContext, 1329 | ) => Promise; 1330 | }) => { 1331 | const item = await this.#getItem(args.denops); 1332 | if (!item || !args.getPreviewer) { 1333 | return ActionFlags.None; 1334 | } 1335 | 1336 | const uiParams = await this.#resolveParams( 1337 | args.denops, 1338 | args.options, 1339 | args.uiParams, 1340 | args.context, 1341 | ); 1342 | 1343 | return this.#previewUi.previewContents( 1344 | args.denops, 1345 | args.context, 1346 | uiParams, 1347 | args.actionParams, 1348 | await this.#getBufnr(args.denops), 1349 | item, 1350 | args.getPreviewer, 1351 | ); 1352 | }, 1353 | previewExecute: async (args: { 1354 | denops: Denops; 1355 | actionParams: BaseParams; 1356 | }) => { 1357 | const command = (args.actionParams as PreviewExecuteParams).command; 1358 | await this.#previewUi.execute(args.denops, command); 1359 | return ActionFlags.Persist; 1360 | }, 1361 | previewPath: async (args: { 1362 | denops: Denops; 1363 | context: Context; 1364 | options: DduOptions; 1365 | uiParams: Params; 1366 | actionParams: BaseParams; 1367 | }) => { 1368 | const item = await this.#getItem(args.denops); 1369 | if (!item) { 1370 | return ActionFlags.None; 1371 | } 1372 | 1373 | await args.denops.cmd(`echo '${item.display ?? item.word}'`); 1374 | 1375 | return ActionFlags.Persist; 1376 | }, 1377 | quit: async (args: { 1378 | denops: Denops; 1379 | context: Context; 1380 | options: DduOptions; 1381 | uiParams: Params; 1382 | actionParams: BaseParams; 1383 | }) => { 1384 | const params = args.actionParams as QuitParams; 1385 | 1386 | await this.#close({ 1387 | denops: args.denops, 1388 | context: args.context, 1389 | options: args.options, 1390 | uiParams: args.uiParams, 1391 | cancel: true, 1392 | }); 1393 | 1394 | await args.denops.cmd("doautocmd User Ddu:uiQuit"); 1395 | 1396 | if (params.force) { 1397 | const bufnr = await this.#getBufnr(args.denops); 1398 | if (bufnr && await fn.bufexists(args.denops, this.#bufferName)) { 1399 | await args.denops.cmd(`bwipeout! ${bufnr}`); 1400 | } 1401 | } else { 1402 | await args.denops.dispatcher.pop(args.options.name); 1403 | } 1404 | 1405 | return ActionFlags.None; 1406 | }, 1407 | redraw: async (args: { 1408 | denops: Denops; 1409 | context: Context; 1410 | options: DduOptions; 1411 | actionParams: BaseParams; 1412 | uiParams: Params; 1413 | }) => { 1414 | if (this.#previewUi.visible()) { 1415 | // Close preview window when redraw 1416 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 1417 | await this.#previewUi.removePreviewedBuffers(args.denops); 1418 | } 1419 | 1420 | // NOTE: await may freeze UI 1421 | const params = args.actionParams as RedrawParams; 1422 | args.denops.dispatcher.redraw(args.options.name, { 1423 | method: params?.method ?? "uiRefresh", 1424 | searchItem: await this.#getItem(args.denops), 1425 | }); 1426 | 1427 | return ActionFlags.None; 1428 | }, 1429 | toggleAllItems: async (args: { 1430 | denops: Denops; 1431 | }) => { 1432 | if (this.#items.length === 0) { 1433 | return Promise.resolve(ActionFlags.None); 1434 | } 1435 | 1436 | this.#items.forEach((item) => { 1437 | if (this.#selectedItems.has(item)) { 1438 | this.#selectedItems.delete(item); 1439 | } else { 1440 | this.#selectedItems.add(item); 1441 | } 1442 | }); 1443 | 1444 | await this.#updateSelectedItems(args.denops); 1445 | 1446 | return Promise.resolve(ActionFlags.Redraw); 1447 | }, 1448 | toggleAutoAction: async (args: { 1449 | denops: Denops; 1450 | context: Context; 1451 | uiParams: Params; 1452 | }) => { 1453 | // Toggle 1454 | this.#enabledAutoAction = !this.#enabledAutoAction; 1455 | 1456 | const winid = await this.#winId({ 1457 | denops: args.denops, 1458 | uiParams: args.uiParams, 1459 | }); 1460 | await this.#setAutoAction(args.denops, args.uiParams, winid); 1461 | 1462 | await this.#doAutoAction(args.denops); 1463 | if (!this.#enabledAutoAction) { 1464 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 1465 | } 1466 | 1467 | return ActionFlags.None; 1468 | }, 1469 | togglePreview: async (args: { 1470 | denops: Denops; 1471 | context: Context; 1472 | options: DduOptions; 1473 | uiParams: Params; 1474 | actionParams: BaseParams; 1475 | getPreviewer?: ( 1476 | denops: Denops, 1477 | item: DduItem, 1478 | actionParams: BaseParams, 1479 | previewContext: PreviewContext, 1480 | ) => Promise; 1481 | }) => { 1482 | const item = await this.#getItem(args.denops); 1483 | if (!item || !args.getPreviewer) { 1484 | return ActionFlags.None; 1485 | } 1486 | 1487 | // Close if the target is the same as the previous one 1488 | if (this.#previewUi.isAlreadyPreviewed(item)) { 1489 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 1490 | return ActionFlags.None; 1491 | } 1492 | 1493 | const uiParams = await this.#resolveParams( 1494 | args.denops, 1495 | args.options, 1496 | args.uiParams, 1497 | args.context, 1498 | ); 1499 | 1500 | return this.#previewUi.previewContents( 1501 | args.denops, 1502 | args.context, 1503 | uiParams, 1504 | args.actionParams, 1505 | await this.#getBufnr(args.denops), 1506 | item, 1507 | args.getPreviewer, 1508 | ); 1509 | }, 1510 | toggleSelectItem: async (args: { 1511 | denops: Denops; 1512 | options: DduOptions; 1513 | uiParams: Params; 1514 | }) => { 1515 | const item = await this.#getItem(args.denops); 1516 | if (!item) { 1517 | return ActionFlags.None; 1518 | } 1519 | 1520 | if (this.#selectedItems.has(item)) { 1521 | this.#selectedItems.delete(item); 1522 | } else { 1523 | this.#selectedItems.add(item); 1524 | } 1525 | 1526 | await this.#updateSelectedItems(args.denops); 1527 | 1528 | return ActionFlags.Redraw; 1529 | }, 1530 | updateOptions: async (args: { 1531 | denops: Denops; 1532 | options: DduOptions; 1533 | actionParams: BaseParams; 1534 | }) => { 1535 | await args.denops.dispatcher.updateOptions( 1536 | args.options.name, 1537 | args.actionParams, 1538 | ); 1539 | return ActionFlags.None; 1540 | }, 1541 | }; 1542 | 1543 | override params(): Params { 1544 | return { 1545 | autoAction: {}, 1546 | autoResize: false, 1547 | cursorPos: 0, 1548 | displaySourceName: "no", 1549 | displayTree: false, 1550 | exprParams: [ 1551 | "previewCol", 1552 | "previewRow", 1553 | "previewHeight", 1554 | "previewWidth", 1555 | "winCol", 1556 | "winRow", 1557 | "winHeight", 1558 | "winWidth", 1559 | ], 1560 | floatingBorder: "none", 1561 | floatingTitle: "", 1562 | floatingTitlePos: "left", 1563 | focus: true, 1564 | highlights: {}, 1565 | ignoreEmpty: false, 1566 | immediateAction: "", 1567 | maxDisplayItems: 1000, 1568 | maxHighlightItems: 100, 1569 | maxWidth: 200, 1570 | onPreview: (_) => { 1571 | return Promise.resolve(); 1572 | }, 1573 | overwriteStatusline: true, 1574 | overwriteTitle: false, 1575 | pathFilter: "", 1576 | previewCol: 0, 1577 | previewFocusable: true, 1578 | previewFloating: false, 1579 | previewFloatingBorder: "none", 1580 | previewFloatingTitle: "", 1581 | previewFloatingTitlePos: "left", 1582 | previewFloatingZindex: 100, 1583 | previewHeight: 10, 1584 | previewMaxSize: 1000000, 1585 | previewRow: 0, 1586 | previewSplit: "horizontal", 1587 | previewWidth: 80, 1588 | previewWindowOptions: [ 1589 | ["&signcolumn", "no"], 1590 | ["&foldcolumn", 0], 1591 | ["&foldenable", 0], 1592 | ["&number", 0], 1593 | ["&wrap", 0], 1594 | ], 1595 | reversed: false, 1596 | replaceCol: 0, 1597 | split: "horizontal", 1598 | splitDirection: "botright", 1599 | startAutoAction: false, 1600 | winCol: "(&columns - eval(uiParams.winWidth)) / 2", 1601 | winHeight: 20, 1602 | winRow: "&lines / 2 - 10", 1603 | winWidth: "&columns / 2", 1604 | }; 1605 | } 1606 | 1607 | async #close(args: { 1608 | denops: Denops; 1609 | context: Context; 1610 | options: DduOptions; 1611 | uiParams: Params; 1612 | cancel: boolean; 1613 | }): Promise { 1614 | // NOTE: this.#close() may be multiple called by WinClosed autocmd. 1615 | if (this.#closing) { 1616 | return; 1617 | } 1618 | 1619 | this.#closing = true; 1620 | 1621 | try { 1622 | await this.#closeWindows(args); 1623 | } finally { 1624 | this.#closing = false; 1625 | } 1626 | } 1627 | 1628 | async #closeWindows(args: { 1629 | denops: Denops; 1630 | context: Context; 1631 | options: DduOptions; 1632 | uiParams: Params; 1633 | cancel: boolean; 1634 | }): Promise { 1635 | await this.#previewUi.close(args.denops, args.context, args.uiParams); 1636 | await this.#previewUi.removePreviewedBuffers(args.denops); 1637 | await args.denops.call("ddu#ui#ff#_reset_auto_action"); 1638 | 1639 | // Move to the UI window. 1640 | const bufnr = await this.#getBufnr(args.denops); 1641 | if (!bufnr) { 1642 | return; 1643 | } 1644 | 1645 | const split = args.uiParams.split; 1646 | if ( 1647 | split === "floating" && 1648 | args.denops.meta.host !== "nvim" && this.#popupId > 0 1649 | ) { 1650 | // Focus to the previous window 1651 | await fn.win_gotoid(args.denops, args.context.winId); 1652 | 1653 | // Close popup 1654 | await args.denops.call("popup_close", this.#popupId); 1655 | await args.denops.cmd("redraw!"); 1656 | this.#popupId = -1; 1657 | } else { 1658 | const winIds = await fn.win_findbuf(args.denops, bufnr) as number[]; 1659 | for (const winId of winIds) { 1660 | if (winId <= 0) { 1661 | continue; 1662 | } 1663 | 1664 | if ( 1665 | (split === "tab" && await fn.tabpagenr(args.denops, "$") > 1) || 1666 | (split !== "no" && await fn.winnr(args.denops, "$") > 1) 1667 | ) { 1668 | await fn.win_gotoid(args.denops, winId); 1669 | await args.denops.cmd("close!"); 1670 | 1671 | // Focus to the previous window 1672 | await fn.win_gotoid(args.denops, args.context.winId); 1673 | } else { 1674 | await fn.win_gotoid(args.denops, winId); 1675 | 1676 | await fn.setwinvar(args.denops, winId, "&winfixbuf", false); 1677 | 1678 | const prevName = await fn.bufname(args.denops, args.context.bufNr); 1679 | await args.denops.cmd( 1680 | prevName !== args.context.bufName || args.context.bufNr == bufnr || 1681 | args.context.bufNr <= 0 1682 | ? "enew" 1683 | : `buffer ${args.context.bufNr}`, 1684 | ); 1685 | } 1686 | } 1687 | } 1688 | 1689 | // Restore mode 1690 | if (this.#saveMode === "i") { 1691 | if (!args.cancel && args.uiParams.replaceCol > 0) { 1692 | const currentLine = await fn.getline(args.denops, "."); 1693 | const replaceLine = currentLine.slice( 1694 | 0, 1695 | args.uiParams.replaceCol - 1, 1696 | ) + currentLine.slice(this.#saveCol - 1); 1697 | await fn.setline(args.denops, ".", replaceLine); 1698 | await fn.cursor(args.denops, 0, args.uiParams.replaceCol - 1); 1699 | } 1700 | 1701 | const endCol = await fn.col(args.denops, "."); 1702 | await fn.feedkeys( 1703 | args.denops, 1704 | args.uiParams.replaceCol > 1 || this.#saveCol > endCol ? "a" : "i", 1705 | "ni", 1706 | ); 1707 | } else if (this.#saveMode === ":") { 1708 | const cmdline = (!args.cancel && args.uiParams.replaceCol > 0) 1709 | ? this.#saveCmdline.slice(0, args.uiParams.replaceCol - 1) + 1710 | this.#saveCmdline.slice(this.#saveCmdpos - 1) 1711 | : this.#saveCmdline; 1712 | const cmdpos = (!args.cancel && args.uiParams.replaceCol > 0) 1713 | ? args.uiParams.replaceCol 1714 | : this.#saveCmdpos; 1715 | 1716 | await args.denops.call( 1717 | "ddu#ui#ff#_restore_cmdline", 1718 | cmdline, 1719 | cmdpos, 1720 | ); 1721 | } 1722 | 1723 | if ( 1724 | this.#restcmd !== "" && 1725 | equal(this.#prevWinInfo, await getWinInfo(args.denops)) 1726 | ) { 1727 | // Restore the layout. 1728 | await args.denops.cmd(this.#restcmd); 1729 | this.#restcmd = ""; 1730 | this.#prevWinInfo = null; 1731 | } 1732 | 1733 | await args.denops.dispatcher.event(args.options.name, "close"); 1734 | } 1735 | 1736 | async #getItem( 1737 | denops: Denops, 1738 | ): Promise { 1739 | const idx = await this.#getIndex(denops); 1740 | return this.#items[idx]; 1741 | } 1742 | 1743 | #getSelectedItems(): DduItem[] { 1744 | return this.#selectedItems.values(); 1745 | } 1746 | 1747 | async #getItems(denops: Denops): Promise { 1748 | let items: DduItem[]; 1749 | if (this.#selectedItems.size() === 0) { 1750 | const item = await this.#getItem(denops); 1751 | if (!item) { 1752 | return []; 1753 | } 1754 | 1755 | items = [item]; 1756 | } else { 1757 | items = this.#getSelectedItems(); 1758 | } 1759 | 1760 | return items.filter((item) => item); 1761 | } 1762 | 1763 | async #collapseItemAction(denops: Denops, options: DduOptions) { 1764 | let item = await this.#getItem(denops); 1765 | if (!item || !item.treePath) { 1766 | return ActionFlags.None; 1767 | } 1768 | 1769 | if (!item.isTree || !item.__expanded) { 1770 | // Use parent item instead. 1771 | const treePath = typeof item.treePath === "string" 1772 | ? item.treePath.split(pathsep) 1773 | : item.treePath; 1774 | const parentPath = treePath.slice(0, -1); 1775 | 1776 | const parent = this.#items.find( 1777 | (itm) => 1778 | equal( 1779 | parentPath, 1780 | typeof itm.treePath === "string" 1781 | ? itm.treePath.split(pathsep) 1782 | : itm.treePath, 1783 | ), 1784 | ); 1785 | 1786 | if (!parent?.treePath || !parent?.isTree || !parent?.__expanded) { 1787 | return ActionFlags.None; 1788 | } 1789 | 1790 | item = parent; 1791 | } 1792 | 1793 | await denops.dispatcher.redrawTree( 1794 | options.name, 1795 | "collapse", 1796 | [{ item }], 1797 | ); 1798 | 1799 | return ActionFlags.None; 1800 | } 1801 | 1802 | async #initOptions( 1803 | denops: Denops, 1804 | options: DduOptions, 1805 | uiParams: Params, 1806 | bufnr: number, 1807 | ): Promise { 1808 | const winid = await this.#winId({ 1809 | denops, 1810 | uiParams, 1811 | }); 1812 | const tabNr = await fn.tabpagenr(denops); 1813 | const existsStatusColumn = await fn.exists(denops, "+statuscolumn"); 1814 | const existsWinFixBuf = await fn.exists(denops, "+winfixbuf"); 1815 | 1816 | await batch(denops, async (denops: Denops) => { 1817 | await fn.setbufvar(denops, bufnr, "ddu_ui_name", options.name); 1818 | await fn.settabvar(denops, tabNr, "ddu_ui_name", options.name); 1819 | 1820 | // Set options 1821 | await fn.setwinvar(denops, winid, "&list", 0); 1822 | await fn.setwinvar(denops, winid, "&foldenable", 0); 1823 | await fn.setwinvar(denops, winid, "&number", 0); 1824 | await fn.setwinvar(denops, winid, "&relativenumber", 0); 1825 | await fn.setwinvar(denops, winid, "&spell", 0); 1826 | await fn.setwinvar(denops, winid, "&wrap", 0); 1827 | await fn.setwinvar(denops, winid, "&colorcolumn", ""); 1828 | await fn.setwinvar(denops, winid, "&foldcolumn", 0); 1829 | await fn.setwinvar(denops, winid, "&signcolumn", "no"); 1830 | if (existsStatusColumn) { 1831 | await fn.setwinvar(denops, winid, "&statuscolumn", ""); 1832 | } 1833 | if ( 1834 | existsWinFixBuf && uiParams.split !== "no" && uiParams.split !== "tab" 1835 | ) { 1836 | await fn.setwinvar(denops, winid, "&winfixbuf", true); 1837 | } 1838 | 1839 | await fn.setbufvar(denops, bufnr, "&bufhidden", "hide"); 1840 | await fn.setbufvar(denops, bufnr, "&buftype", "nofile"); 1841 | await fn.setbufvar(denops, bufnr, "&filetype", "ddu-ff"); 1842 | await fn.setbufvar(denops, bufnr, "&swapfile", 0); 1843 | 1844 | if (uiParams.split === "horizontal") { 1845 | await fn.setwinvar(denops, winid, "&winfixheight", 1); 1846 | } else if (uiParams.split === "vertical") { 1847 | await fn.setwinvar(denops, winid, "&winfixwidth", 1); 1848 | } 1849 | 1850 | if (uiParams.split === "floating") { 1851 | await fn.setwinvar(denops, winid, "&statusline", ""); 1852 | } 1853 | }); 1854 | } 1855 | 1856 | async #resolveParams( 1857 | denops: Denops, 1858 | options: DduOptions, 1859 | uiParams: Params, 1860 | context: Record, 1861 | ): Promise { 1862 | const defaults = this.params(); 1863 | 1864 | context = { 1865 | sources: options.sources.map( 1866 | (source) => is.String(source) ? source : source.name, 1867 | ), 1868 | itemCount: this.#items.length, 1869 | uiParams, 1870 | ...context, 1871 | }; 1872 | 1873 | const params = Object.assign(uiParams); 1874 | for (const name of uiParams.exprParams) { 1875 | if (name in uiParams) { 1876 | params[name] = await evalExprParam( 1877 | denops, 1878 | name, 1879 | params[name], 1880 | defaults[name], 1881 | context, 1882 | ); 1883 | } else { 1884 | await printError( 1885 | denops, 1886 | `Invalid expr param: ${name}`, 1887 | ); 1888 | } 1889 | } 1890 | 1891 | return params; 1892 | } 1893 | 1894 | async #getBufnr( 1895 | denops: Denops, 1896 | ): Promise { 1897 | return this.#bufferName.length === 0 1898 | ? -1 1899 | : await fn.bufnr(denops, this.#bufferName); 1900 | } 1901 | 1902 | async #getIndex( 1903 | denops: Denops, 1904 | ): Promise { 1905 | const bufnr = await this.#getBufnr(denops); 1906 | const cursorPos = await fn.getbufvar( 1907 | denops, 1908 | bufnr, 1909 | "ddu_ui_ff_cursor_pos", 1910 | [], 1911 | ) as CursorPos; 1912 | if (cursorPos.length === 0) { 1913 | return -1; 1914 | } 1915 | 1916 | const viewItem = this.#viewItems[cursorPos[1] - 1]; 1917 | return this.#items.findIndex( 1918 | (item: DduItem) => equal(item, viewItem), 1919 | ); 1920 | } 1921 | 1922 | #getItemIndex(viewItem: DduItem): number { 1923 | return this.#items.findIndex( 1924 | (item: DduItem) => equal(item, viewItem), 1925 | ); 1926 | } 1927 | 1928 | async #doAutoAction(denops: Denops) { 1929 | if (this.#enabledAutoAction) { 1930 | await denops.call("ddu#ui#ff#_do_auto_action"); 1931 | } 1932 | } 1933 | 1934 | async #setAutoAction(denops: Denops, uiParams: Params, winId: number) { 1935 | const hasAutoAction = "name" in uiParams.autoAction && 1936 | this.#enabledAutoAction; 1937 | 1938 | await batch(denops, async (denops: Denops) => { 1939 | await denops.call("ddu#ui#ff#_reset_auto_action"); 1940 | if (hasAutoAction) { 1941 | const autoAction = Object.assign( 1942 | { delay: 100, params: {}, sync: true }, 1943 | uiParams.autoAction, 1944 | ); 1945 | await denops.call( 1946 | "ddu#ui#ff#_set_auto_action", 1947 | winId, 1948 | autoAction, 1949 | ); 1950 | } 1951 | }); 1952 | } 1953 | 1954 | async #cursor( 1955 | denops: Denops, 1956 | context: Context, 1957 | pos: CursorPos, 1958 | ): Promise { 1959 | if (pos.length !== 0) { 1960 | await fn.cursor(denops, pos); 1961 | await vars.b.set( 1962 | denops, 1963 | "ddu_ui_ff_cursor_pos", 1964 | await fn.getcurpos(denops), 1965 | ); 1966 | 1967 | await this.#doAutoAction(denops); 1968 | } 1969 | 1970 | const newPos = await fn.getcurpos(denops); 1971 | if (pos[0]) { 1972 | newPos[1] = pos[0]; 1973 | } 1974 | if (pos[1]) { 1975 | newPos[2] = pos[1]; 1976 | } 1977 | 1978 | await this.updateCursor({ denops, context }); 1979 | } 1980 | 1981 | async #updateSelectedItems( 1982 | denops: Denops, 1983 | ) { 1984 | const setItems = new ObjectSet(this.#items); 1985 | const toDelete = new ObjectSet(); 1986 | 1987 | this.#selectedItems.forEach((item) => { 1988 | if (!setItems.has(item)) { 1989 | toDelete.add(item); 1990 | } 1991 | }); 1992 | 1993 | toDelete.forEach((item) => this.#selectedItems.delete(item)); 1994 | 1995 | await fn.setbufvar( 1996 | denops, 1997 | await this.#getBufnr(denops), 1998 | "ddu_ui_selected_items", 1999 | this.#getSelectedItems(), 2000 | ); 2001 | } 2002 | 2003 | async #clearSavedCursor(denops: Denops) { 2004 | const bufnr = await fn.bufnr(denops, this.#bufferName); 2005 | if (bufnr > 0) { 2006 | await fn.setbufvar(denops, bufnr, "ddu_ui_item", {}); 2007 | } 2008 | } 2009 | } 2010 | 2011 | async function initBuffer( 2012 | denops: Denops, 2013 | bufferName: string, 2014 | ): Promise { 2015 | const bufnr = await fn.bufadd(denops, bufferName); 2016 | await fn.setbufvar(denops, bufnr, "&modifiable", false); 2017 | await fn.bufload(denops, bufnr); 2018 | return bufnr; 2019 | } 2020 | 2021 | async function evalExprParam( 2022 | denops: Denops, 2023 | name: string, 2024 | expr: string | unknown, 2025 | defaultExpr: string | unknown, 2026 | context: Record, 2027 | ): Promise { 2028 | if (!is.String(expr)) { 2029 | return expr; 2030 | } 2031 | 2032 | try { 2033 | return await denops.eval(expr, context); 2034 | } catch (e) { 2035 | await printError( 2036 | denops, 2037 | e, 2038 | `[ddu-ui-ff] invalid expression in option: ${name}`, 2039 | ); 2040 | 2041 | // Fallback to default param. 2042 | return is.String(defaultExpr) 2043 | ? await denops.eval(defaultExpr, context) 2044 | : defaultExpr; 2045 | } 2046 | } 2047 | 2048 | async function getWinInfo( 2049 | denops: Denops, 2050 | ): Promise { 2051 | return { 2052 | columns: await op.columns.getGlobal(denops), 2053 | lines: await op.lines.getGlobal(denops), 2054 | winid: await fn.win_getid(denops), 2055 | tabpagebuflist: await fn.tabpagebuflist(denops) as number[], 2056 | }; 2057 | } 2058 | 2059 | async function setStatusline( 2060 | denops: Denops, 2061 | context: Context, 2062 | options: DduOptions, 2063 | uiParams: Params, 2064 | winid: number, 2065 | augroupName: string, 2066 | items: DduItem[], 2067 | ): Promise { 2068 | const statusState = { 2069 | done: context.done, 2070 | input: context.input, 2071 | name: options.name, 2072 | maxItems: context.maxItems, 2073 | }; 2074 | await fn.setwinvar( 2075 | denops, 2076 | winid, 2077 | "ddu_ui_ff_status", 2078 | statusState, 2079 | ); 2080 | 2081 | const header = `[ddu-${options.name}]` + 2082 | (items.length !== context.maxItems 2083 | ? ` ${items.length}/${context.maxItems}` 2084 | : ""); 2085 | 2086 | const linenr = 2087 | "printf('%'.(('$'->line())->len()+2).'d/%d','.'->line(),'$'->line())"; 2088 | const laststatus = await op.laststatus.getGlobal(denops); 2089 | 2090 | const input = `${context.input.length > 0 ? " " + context.input : ""}`; 2091 | const async = `${ 2092 | context.done || await fn.mode(denops) == "c" ? "" : " [async]" 2093 | }`; 2094 | const footer = `${input}${async}`; 2095 | 2096 | if (laststatus === 0 || uiParams.overwriteTitle) { 2097 | if (await vars.g.get(denops, "ddu#ui#ff#_save_title", "") === "") { 2098 | await vars.g.set( 2099 | denops, 2100 | "ddu#ui#ff#_save_title", 2101 | await op.titlestring.getGlobal(denops), 2102 | ); 2103 | } 2104 | 2105 | await denops.cmd( 2106 | `autocmd ${augroupName} WinClosed,BufLeave ` + 2107 | " let &titlestring=g:ddu#ui#ff#_save_title", 2108 | ); 2109 | 2110 | const titleString = `${header} %{${linenr}}%*${footer}`; 2111 | await vars.b.set(denops, "ddu_ui_ff_title", titleString); 2112 | await op.titlestring.setGlobal(denops, titleString); 2113 | 2114 | await denops.cmd( 2115 | `autocmd ${augroupName} WinEnter,BufEnter ` + 2116 | " let &titlestring=b:->get('ddu_ui_ff_title', '')", 2117 | ); 2118 | } else if (uiParams.overwriteStatusline) { 2119 | await fn.setwinvar( 2120 | denops, 2121 | winid, 2122 | "&statusline", 2123 | `${header.replaceAll("%", "%%")} %#LineNR#%{${linenr}}%*${footer}`, 2124 | ); 2125 | } 2126 | } 2127 | 2128 | class ObjectSet { 2129 | #items: T[] = []; 2130 | 2131 | constructor(initialItems?: T[]) { 2132 | if (initialItems) { 2133 | this.#items = [...initialItems]; 2134 | } 2135 | } 2136 | 2137 | add(item: T): void { 2138 | if (!this.has(item)) { 2139 | this.#items.push(item); 2140 | } 2141 | } 2142 | 2143 | has(item: T): boolean { 2144 | return this.#items.some((existingItem) => equal(existingItem, item)); 2145 | } 2146 | 2147 | clear(): void { 2148 | this.#items = []; 2149 | } 2150 | 2151 | size(): number { 2152 | return this.#items.length; 2153 | } 2154 | 2155 | delete(item: T): boolean { 2156 | const index = this.#items.findIndex((existingItem) => 2157 | equal(existingItem, item) 2158 | ); 2159 | if (index !== -1) { 2160 | this.#items.splice(index, 1); 2161 | return true; 2162 | } 2163 | return false; 2164 | } 2165 | 2166 | values(): T[] { 2167 | return [...this.#items]; 2168 | } 2169 | 2170 | forEach(callback: (item: T, index: number, array: T[]) => void): void { 2171 | this.#items.forEach(callback); 2172 | } 2173 | } 2174 | --------------------------------------------------------------------------------