├── .gitignore ├── .npmignore ├── tsconfig.json ├── esbuild.js ├── src ├── util.ts ├── db.ts ├── list │ └── yank.ts └── index.ts ├── Readme.md ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | tslint.json 4 | *.map 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@chemzqm/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "lib", 6 | "target": "es2017", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "lib": ["es2018"], 10 | "plugins": [] 11 | }, 12 | "include": ["src"], 13 | "exclude": [] 14 | } 15 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | 2 | async function start() { 3 | await require('esbuild').build({ 4 | entryPoints: ['src/index.ts'], 5 | bundle: true, 6 | minify: process.env.NODE_ENV === 'production', 7 | sourcemap: process.env.NODE_ENV === 'development', 8 | mainFields: ['module', 'main'], 9 | external: ['coc.nvim'], 10 | platform: 'node', 11 | target: 'node16.18', 12 | outfile: 'lib/index.js' 13 | }) 14 | } 15 | 16 | start().catch(e => { 17 | console.error(e) 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import util from 'util' 3 | 4 | export async function statAsync(filepath: string): Promise { 5 | let stat = null 6 | try { 7 | stat = await util.promisify(fs.stat)(filepath) 8 | } catch (e) { } // tslint:disable-line 9 | return stat 10 | } 11 | 12 | export async function writeFile(fullpath, content: string): Promise { 13 | await util.promisify(fs.writeFile)(fullpath, content, 'utf8') 14 | } 15 | 16 | export function readFile(fullpath: string, encoding = 'utf8'): Promise { 17 | return new Promise((resolve, reject) => { 18 | fs.readFile(fullpath, encoding, (err, content) => { 19 | if (err) reject(err) 20 | resolve(content) 21 | }) 22 | }) 23 | } 24 | 25 | export function mkdirAsync(filepath: string): Promise { 26 | return new Promise((resolve, reject) => { 27 | fs.mkdir(filepath, err => { 28 | if (err) return reject(err) 29 | resolve() 30 | }) 31 | }) 32 | } 33 | 34 | export function group(array: T[], size: number): T[][] { 35 | let len = array.length 36 | let res: T[][] = [] 37 | for (let i = 0; i < Math.ceil(len / size); i++) { 38 | res.push(array.slice(i * size, (i + 1) * size)) 39 | } 40 | return res 41 | } 42 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # coc-yank 2 | 3 | Yank extension for [coc.nvim](https://github.com/neoclide/coc.nvim). 4 | 5 | Note, make sure you have TextYankPost autocmd with your vim by 6 | `:echo exists('##TextYankPost')` 7 | 8 | ## Install 9 | 10 | In your vim/neovim, run command: 11 | 12 | ``` 13 | :CocInstall coc-yank 14 | ``` 15 | 16 | Setup keymap to open yank list like: 17 | 18 | ``` 19 | nnoremap y :CocList -A --normal yank 20 | ``` 21 | 22 | `-A` means auto preview, and `--normal` means open list on normal mode. 23 | 24 | ## Features 25 | 26 | - Highlight yanked text. 27 | - Persist yank list across vim instances. 28 | 29 | ## Options 30 | 31 | - `yank.highlight.enable` enable highlight feature, default: `true`. 32 | - `yank.highlight.duration` duration of highlight in milliseconds, default: 500. 33 | - `yank.list.maxsize` maxsize of yank list, default: 200 34 | - `yank.enableCompletion`: Enable completion support for yanked text, default: `true` 35 | - `yank.priority`: Priority of yank completion source, default: 90. 36 | - `yank.limit`: Max completion item count from yank history. 37 | 38 | ## F.A.Q 39 | 40 | Q: How to change highlight color? 41 | 42 | A: Add `hi HighlightedyankRegion term=bold ctermbg=0 guibg=#13354A` to your 43 | `.vimrc` after `:colorscheme` command. 44 | 45 | Q: How to clear all yanks? 46 | 47 | A: In vim, `:CocCommand yank.clean` 48 | 49 | ## License 50 | 51 | MIT 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-yank", 3 | "version": "1.2.5", 4 | "description": "Yank extension for coc.nvim", 5 | "main": "lib/index.js", 6 | "publisher": "chemzqm", 7 | "keywords": [ 8 | "coc.nvim", 9 | "yank", 10 | "colors" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/neoclide/coc-yank.git" 15 | }, 16 | "engines": { 17 | "coc": "^0.0.82" 18 | }, 19 | "scripts": { 20 | "build": "node esbuild.js", 21 | "prepare": "node esbuild.js" 22 | }, 23 | "activationEvents": [ 24 | "*" 25 | ], 26 | "contributes": { 27 | "configuration": { 28 | "type": "object", 29 | "properties": { 30 | "yank.highlight.enable": { 31 | "type": "boolean", 32 | "default": true, 33 | "description": "Enable highlight of yanked text." 34 | }, 35 | "yank.highlight.duration": { 36 | "type": "number", 37 | "default": 500, 38 | "description": "Duration of highlight in milliseconds." 39 | }, 40 | "yank.list.maxsize": { 41 | "type": "number", 42 | "default": 300, 43 | "description": "Maximum size of preserved yank list." 44 | }, 45 | "yank.byteLengthLimit": { 46 | "type": "number", 47 | "default": 10240, 48 | "description": "Maximum byte length of yanked text which could be preserved." 49 | }, 50 | "yank.enableCompletion": { 51 | "type": "boolean", 52 | "description": "Enable completion support for yanked text.", 53 | "default": true 54 | }, 55 | "yank.priority": { 56 | "type": "integer", 57 | "description": "Priority of yank completion source.", 58 | "default": 90 59 | }, 60 | "yank.limit": { 61 | "type": "integer", 62 | "description": "Max completion item count from yank history.", 63 | "default": 3 64 | } 65 | } 66 | } 67 | }, 68 | "author": "chemzqm@gmail.com", 69 | "license": "MIT", 70 | "devDependencies": { 71 | "@chemzqm/tsconfig": "^0.0.3", 72 | "@types/node": "^16.18", 73 | "esbuild": "^0.25.0", 74 | "coc.nvim": "^0.0.83-next.19", 75 | "colors": "^1.4.0" 76 | }, 77 | "dependencies": {} 78 | } 79 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@chemzqm/tsconfig@^0.0.3": 6 | version "0.0.3" 7 | resolved "https://registry.npmjs.org/@chemzqm/tsconfig/-/tsconfig-0.0.3.tgz" 8 | integrity sha512-MjF25vbqLYR+S+JJLgBi0vn4gZqv/C87H+yPSlVKEqlIJAJOGJOgFPUFvRS7pdRHqkv2flX/oRxzxhlu2V0X1w== 9 | 10 | "@esbuild/darwin-x64@0.25.0": 11 | version "0.25.0" 12 | resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz" 13 | integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== 14 | 15 | "@types/node@^16.18", "@types/node@^16.18.0": 16 | version "16.18.126" 17 | resolved "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz" 18 | integrity sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw== 19 | 20 | coc.nvim@^0.0.83-next.19: 21 | version "0.0.83-next.19" 22 | resolved "https://registry.npmjs.org/coc.nvim/-/coc.nvim-0.0.83-next.19.tgz" 23 | integrity sha512-OuIti7VPDecpaumEx+2M7v6G0SCOIxskbMKPD3G0b3z+3lqx/fzP/WJvDdPJTJ6LvEimlmvIsRKWoyTncqhmHg== 24 | 25 | colors@^1.4.0: 26 | version "1.4.0" 27 | resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" 28 | integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== 29 | 30 | esbuild@^0.25.0: 31 | version "0.25.0" 32 | resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz" 33 | integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== 34 | optionalDependencies: 35 | "@esbuild/aix-ppc64" "0.25.0" 36 | "@esbuild/android-arm" "0.25.0" 37 | "@esbuild/android-arm64" "0.25.0" 38 | "@esbuild/android-x64" "0.25.0" 39 | "@esbuild/darwin-arm64" "0.25.0" 40 | "@esbuild/darwin-x64" "0.25.0" 41 | "@esbuild/freebsd-arm64" "0.25.0" 42 | "@esbuild/freebsd-x64" "0.25.0" 43 | "@esbuild/linux-arm" "0.25.0" 44 | "@esbuild/linux-arm64" "0.25.0" 45 | "@esbuild/linux-ia32" "0.25.0" 46 | "@esbuild/linux-loong64" "0.25.0" 47 | "@esbuild/linux-mips64el" "0.25.0" 48 | "@esbuild/linux-ppc64" "0.25.0" 49 | "@esbuild/linux-riscv64" "0.25.0" 50 | "@esbuild/linux-s390x" "0.25.0" 51 | "@esbuild/linux-x64" "0.25.0" 52 | "@esbuild/netbsd-arm64" "0.25.0" 53 | "@esbuild/netbsd-x64" "0.25.0" 54 | "@esbuild/openbsd-arm64" "0.25.0" 55 | "@esbuild/openbsd-x64" "0.25.0" 56 | "@esbuild/sunos-x64" "0.25.0" 57 | "@esbuild/win32-arm64" "0.25.0" 58 | "@esbuild/win32-ia32" "0.25.0" 59 | "@esbuild/win32-x64" "0.25.0" 60 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import crypto from 'crypto' 4 | import { readFile } from './util' 5 | 6 | export interface HistoryItem { 7 | id: string 8 | content: string[] 9 | regtype: string 10 | path: string 11 | filetype: string 12 | } 13 | 14 | export default class DB { 15 | private file: string 16 | 17 | constructor(directory: string, private maxsize: number) { 18 | let jsonFile = path.join(directory, 'yank.json') 19 | this.file = path.join(directory, 'yank') 20 | this.convertJson(jsonFile) 21 | } 22 | 23 | private buildItem(content: string[], filepath: string, regtype: string, filetype: string): HistoryItem { 24 | let id = crypto.createHash('md5').update(content.join('\n')).digest('hex') 25 | 26 | return { 27 | id, 28 | content, 29 | path: filepath, 30 | filetype, 31 | regtype, 32 | } 33 | } 34 | 35 | private write(rawItems: Array): void { 36 | let lines: string[] = [] 37 | let items = rawItems; 38 | 39 | if (items.length > this.maxsize) { 40 | items = items.slice(items.length - this.maxsize); 41 | } 42 | 43 | for (let item of items) { 44 | let [filepath, lnum, col] = item.path.split('\t') 45 | let line = `${item.id}|${filepath}|${lnum}|${col}|${item.regtype}|${item.filetype}` 46 | lines.push(line) 47 | lines.push(...item.content.map(s => `\t${s}`)) 48 | } 49 | 50 | fs.writeFileSync(this.file, lines.join('\n') + '\n', 'utf8') 51 | } 52 | 53 | private convertJson(jsonFile: string): void { 54 | if (!fs.existsSync(jsonFile)) return 55 | try { 56 | let content = fs.readFileSync(jsonFile, 'utf8') 57 | let items = JSON.parse(content).map( 58 | ({ content, filepath, regtype, filetype }) => this.buildItem(content, filepath, regtype, filetype) 59 | ) 60 | this.write(items) 61 | } catch (_e) { 62 | // noop 63 | } 64 | fs.unlinkSync(jsonFile) 65 | } 66 | 67 | public clean(): void { 68 | if (fs.existsSync(this.file)) { 69 | fs.unlinkSync(this.file) 70 | } 71 | } 72 | 73 | public async load(): Promise { 74 | if (!fs.existsSync(this.file)) return [] 75 | let items: HistoryItem[] = [] 76 | try { 77 | let content = await readFile(this.file) 78 | let lines: string[] = [] 79 | let item: HistoryItem = null 80 | for (let line of content.split(/\r?\n/)) { 81 | if (line.startsWith('\t')) { 82 | lines.push(line.slice(1)) 83 | } else { 84 | if (item) { 85 | item.content = lines 86 | items.push(item) 87 | lines = [] 88 | } 89 | let [hash, path, lnum, col, regtype, filetype] = line.split('|') 90 | item = { 91 | id: hash, 92 | path: `${path}\t${lnum}\t${col}`, 93 | regtype, 94 | filetype, 95 | content: [] 96 | } 97 | } 98 | } 99 | return items 100 | } catch (e) { 101 | return [] 102 | } 103 | } 104 | 105 | public async add(content: string[], regtype: string, filepath: string, filetype: string): Promise { 106 | let item = this.buildItem(content, filepath, regtype, filetype) 107 | let items = await this.load() 108 | let idx = items.findIndex(o => o.id == item.id) 109 | if (idx != -1) return 110 | items.push(item) 111 | this.write(items) 112 | } 113 | 114 | public async delete(id: string | string[]): Promise { 115 | let items = await this.load() 116 | items = items.filter(o => { 117 | if (typeof id == 'string') return o.id != id 118 | return id.indexOf(o.id) == -1 119 | }) 120 | 121 | this.write(items); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/list/yank.ts: -------------------------------------------------------------------------------- 1 | import { BasicList, ListContext, ListItem, Neovim, Position, Range, TextEdit, window, workspace } from 'coc.nvim' 2 | import colors from 'colors/safe' 3 | import DB, { HistoryItem } from '../db' 4 | 5 | export default class YankList extends BasicList { 6 | public readonly name = 'yank' 7 | public readonly description = 'list of yank history' 8 | public defaultAction = 'append' 9 | 10 | constructor(nvim: Neovim, private db: DB) { 11 | super() 12 | 13 | this.addAction('append', async (item: ListItem) => { 14 | let { document, position } = await workspace.getCurrentState() 15 | let doc = workspace.getDocument(document.uri) 16 | if (!doc || !doc.attached) { 17 | window.showWarningMessage(`Current document not attached.`) 18 | return 19 | } 20 | let edits: TextEdit[] = [] 21 | let { regtype, content } = item.data as HistoryItem 22 | let line = doc.getline(position.line) 23 | if (regtype == 'v') { 24 | let pos = Position.create(position.line, Math.min(position.character + 1, line.length)) 25 | edits.push({ 26 | range: Range.create(pos, pos), 27 | newText: content.join('\n') 28 | }) 29 | } else if (regtype == 'V') { 30 | let pos = Position.create(position.line + 1, 0) 31 | edits.push({ 32 | range: Range.create(pos, pos), 33 | newText: content.join('\n') + '\n' 34 | }) 35 | } else { 36 | let col = await nvim.call('col', ['.']) as number 37 | for (let i = position.line; i < position.line + content.length; i++) { 38 | let line = doc.getline(i) 39 | let character = byteSlice(line, 0, col + 1).length 40 | let pos = Position.create(i, character) 41 | edits.push({ 42 | range: Range.create(pos, pos), 43 | newText: content[i - position.line] 44 | }) 45 | } 46 | } 47 | await doc.applyEdits(edits) 48 | }) 49 | 50 | this.addAction('prepend', async (item: ListItem) => { 51 | let { document, position } = await workspace.getCurrentState() 52 | let doc = workspace.getDocument(document.uri) 53 | if (!doc || !doc.attached) { 54 | window.showMessage(`Current document not attached.`) 55 | return 56 | } 57 | let edits: TextEdit[] = [] 58 | let { regtype, content } = item.data as HistoryItem 59 | if (regtype == 'v') { 60 | let pos = Position.create(position.line, position.character) 61 | edits.push({ 62 | range: Range.create(pos, pos), 63 | newText: content.join('\n') 64 | }) 65 | } else if (regtype == 'V') { 66 | let pos = Position.create(position.line, 0) 67 | edits.push({ 68 | range: Range.create(pos, pos), 69 | newText: content.join('\n') + '\n' 70 | }) 71 | } else { 72 | let col = await nvim.call('col', ['.']) as number 73 | for (let i = position.line; i < position.line + content.length; i++) { 74 | let line = doc.getline(i) 75 | let character = byteSlice(line, 0, col).length 76 | let pos = Position.create(i, character) 77 | edits.push({ 78 | range: Range.create(pos, pos), 79 | newText: content[i - position.line] 80 | }) 81 | } 82 | } 83 | await doc.applyEdits(edits) 84 | }) 85 | 86 | this.addAction('open', async (item: ListItem) => { 87 | let content = item.data.path as string 88 | let parts = content.split('\t') 89 | let position = Position.create(Number(parts[1]) - 1, Number(parts[2]) - 1) 90 | await workspace.jumpTo(parts[0], position) 91 | }) 92 | 93 | this.addAction('yank', (item: ListItem) => { 94 | let content = item.data.content as string[] 95 | content = content.map(s => s.replace(/\\/g, '\\\\').replace(/"/, '\\"')) 96 | nvim.command(`let @" = "${content.join('\\n')}"`, true) 97 | }) 98 | 99 | this.addMultipleAction('delete', async (items: ListItem[]) => { 100 | let ids = items.map(o => o.data.id) 101 | await this.db.delete(ids) 102 | }, { persist: true, reload: true }) 103 | 104 | this.addAction('preview', async (item: ListItem, context) => { 105 | let { filetype, content } = item.data as HistoryItem 106 | this.preview({ 107 | sketch: true, 108 | filetype, 109 | lines: content 110 | }, context) 111 | }) 112 | } 113 | 114 | public async loadItems(_context: ListContext): Promise { 115 | let arr = await this.db.load() 116 | let columns = await this.nvim.getOption('columns') as number 117 | let res: ListItem[] = [] 118 | for (let item of arr.reverse()) { 119 | let regtype: string 120 | if (item.regtype == 'v') { 121 | regtype = 'char ' 122 | } else if (item.regtype == 'V') { 123 | regtype = 'line ' 124 | } else { 125 | regtype = 'block' 126 | } 127 | let text = item.content.join(' ') 128 | let abbr = text.length > columns - 15 ? text.slice(0, columns - 15) + colors.grey('...') : text 129 | res.push({ 130 | label: `${colors.yellow(regtype)} ${abbr}`, 131 | filterText: abbr, 132 | data: Object.assign({}, item) 133 | }) 134 | } 135 | return res 136 | } 137 | } 138 | 139 | function byteSlice(content: string, start: number, end?: number): string { 140 | let buf = Buffer.from(content, 'utf8') 141 | return buf.slice(start, end).toString('utf8') 142 | } 143 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, ExtensionContext, Position, Range, commands, events, languages, listManager, workspace } from 'coc.nvim' 2 | import DB from './db' 3 | import YankList from './list/yank' 4 | import { mkdirAsync, statAsync } from './util' 5 | 6 | export async function activate(context: ExtensionContext): Promise { 7 | let { subscriptions, logger, storagePath } = context 8 | let stat = await statAsync(storagePath) 9 | if (!stat || !stat.isDirectory()) { 10 | await mkdirAsync(storagePath) 11 | } 12 | const config = workspace.getConfiguration('yank') 13 | let db = new DB(storagePath, config.get('list.maxsize', 200)) 14 | const maxLength = config.get('byteLengthLimit', 10240) 15 | if (config.get('highlight.enable', true)) { 16 | workspace.nvim.command('highlight default link HighlightedyankRegion IncSearch', true) 17 | } 18 | subscriptions.push(listManager.registerList(new YankList(workspace.nvim, db))) 19 | subscriptions.push(commands.registerCommand('yank.clean', () => { 20 | db.clean() 21 | })) 22 | events.on('WinLeave', winid => { 23 | workspace.nvim.call('coc#highlight#clear_match_group', [winid, '^HighlightedyankRegion'], true) 24 | }, null, subscriptions) 25 | subscriptions.push(workspace.registerAutocmd({ 26 | event: 'TextYankPost', 27 | arglist: ['v:event', "+expand('')", 'win_getid()'], 28 | callback: async (event, bufnr, winid) => { 29 | let { nvim } = workspace 30 | let { regtype, operator, regcontents, regname, inclusive } = event; 31 | let doc = workspace.getDocument(bufnr) 32 | if (!doc || !doc.attached) return 33 | let len = 0 34 | for (let s of regcontents) { 35 | len += Buffer.byteLength(s, 'utf8') 36 | } 37 | if (len > maxLength) return 38 | let lnum, col 39 | if ((!await nvim.call('has', 'nvim')) && regname == '*' && !inclusive) { 40 | let [, lnum_1, col_1] = await nvim.call('getpos', ["'<"]) 41 | let [, lnum_2, col_2] = await nvim.call('getpos', ["'>"]) 42 | lnum = lnum_1; 43 | if (col_1 <= col_2) { 44 | col = col_1 45 | } else { 46 | col = col_2 47 | } 48 | } else { 49 | [, lnum, col] = await nvim.call('getpos', ["."]) 50 | } 51 | let character = byteSlice(doc.getline(lnum - 1), 0, col).length 52 | let enableHighlight = config.get('highlight.enable', true) 53 | if (enableHighlight && operator == 'y') { 54 | let ranges: Range[] = [] 55 | let duration = config.get('highlight.duration', 500) 56 | // block selection 57 | if (regtype.startsWith('\x16')) { 58 | let view = await nvim.call('winsaveview') 59 | await nvim.call('setpos', ['.', [0, lnum, col, 0]]) 60 | for (let i = lnum; i < lnum + regcontents.length; i++) { 61 | let line = doc.getline(i - 1) 62 | let startCharacter = byteSlice(line, 0, col - 1).length 63 | let start = Position.create(i - 1, startCharacter) 64 | let end = Position.create(i - 1, startCharacter + regcontents[i - lnum].length) 65 | ranges.push(Range.create(start, end)) 66 | await nvim.command('normal! j') 67 | } 68 | await nvim.call('winrestview', [view]) 69 | } else if (regtype == 'v') { 70 | let start = Position.create(lnum - 1, character) 71 | let endCharacter = regcontents.length == 1 ? character + regcontents[0].length - 1 : regcontents[regcontents.length - 1].length 72 | let end = Position.create(lnum + regcontents.length - 2, endCharacter) 73 | ranges.push(Range.create(start, end)) 74 | } else if (regtype == 'V') { 75 | for (let i = lnum; i < lnum + regcontents.length; i++) { 76 | let line = doc.getline(i - 1) 77 | ranges.push(Range.create(i - 1, 0, i - 1, line.length)) 78 | } 79 | } else { 80 | logger.error(`Unkonw regtype "${regtype}"`) 81 | return 82 | } 83 | nvim.call('coc#highlight#match_ranges', [winid, bufnr, ranges, 'HighlightedyankRegion', 99], true) 84 | setTimeout(() => { 85 | nvim.call('coc#highlight#clear_match_group', [winid, '^HighlightedyankRegion'], true) 86 | if (workspace.isNvim) { 87 | // - Hack for bug where highlight only works 1 time after starting Vim. 88 | // - Can give some flickering, and clears cmd texts. 89 | nvim.command('redraw!') 90 | } else { 91 | nvim.command('redraw', true) 92 | } 93 | }, duration) 94 | } 95 | let content = regcontents.join('\n') 96 | if (content.length < 4) return 97 | let path = `${doc.uri}\t${lnum}\t${col}` 98 | regtype = regtype.startsWith('\x16') ? '^v' : regtype 99 | await db.add(regcontents, regtype, path, doc.filetype) 100 | } 101 | })) 102 | 103 | languages.registerCompletionItemProvider('yank', 'YANK', null, { 104 | provideCompletionItems: async (document, _position, _token, context): Promise => { 105 | const config = workspace.getConfiguration('yank') 106 | let enabled = config.get('enableCompletion', true) 107 | if (!enabled) return [] 108 | let limit = config.get('limit', 3) 109 | let { option } = context as any 110 | if (!option || !option.input) return 111 | let items = await db.load() 112 | items.reverse() 113 | items = items.filter(o => { 114 | if (o.regtype == '^v') return false 115 | return o.filetype == document.languageId && o.content[0].trim().startsWith(option.input) 116 | }) 117 | let before_content = option.line.slice(0, option.col) 118 | if (!/^\s*$/.test(before_content)) { 119 | items = items.filter(o => o.regtype != 'V') 120 | } 121 | items = items.slice(0, limit) 122 | return items.map(item => { 123 | let ind = item.content.reduce((p, s) => { 124 | let ms = s.match(/^\s*/)[0] 125 | return Math.min(ms.length, p) 126 | }, Infinity) 127 | let lines = item.content.map((s, i) => { 128 | if (i == 0) return s.replace(/^\s*/, '') 129 | return s.slice(ind) 130 | }) 131 | return { 132 | label: item.content[0].trim(), 133 | insertText: lines.join('\n'), 134 | kind: CompletionItemKind.Snippet, 135 | documentation: { 136 | kind: 'markdown', 137 | value: markdownBlock(lines.join('\n'), item.filetype) 138 | } 139 | } as CompletionItem 140 | }) 141 | } 142 | }, [], config.get('priority', 9)) 143 | } 144 | 145 | function byteSlice(content: string, start: number, end?: number): string { 146 | let buf = Buffer.from(content, 'utf8') 147 | return buf.slice(start, end).toString('utf8') 148 | } 149 | 150 | function markdownBlock(code: string, filetype: string): string { 151 | filetype = filetype == 'javascriptreact' ? 'javascript' : filetype 152 | filetype = filetype == 'typescriptreact' ? 'typescript' : filetype 153 | return '``` ' + filetype + '\n' + code + '\n```' 154 | } 155 | --------------------------------------------------------------------------------