├── LICENSE ├── Makefile ├── README.md ├── deno.jsonc ├── denops └── @ddc-sources │ └── dictionary.ts └── doc └── ddc-dictionary.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Haruki Matsui 4 | 2019 Shougo Matsushita 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGETS := $$(find . \( -name '*.ts' -or -name '*.md' \) -not -path './.deno/*') 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | help: 6 | @cat $(MAKEFILE_LIST) | \ 7 | perl -ne 'print if /^\w+.*##/;' | \ 8 | perl -pe 's/(.*):.*##\s*/sprintf("%-20s",$$1)/eg;' 9 | 10 | fmt: FORCE ## Format code 11 | @deno fmt 12 | 13 | fmt-check: FORCE ## Format check 14 | @deno fmt --check 15 | 16 | lint: FORCE ## Lint code 17 | @deno lint 18 | 19 | type-check: FORCE ## Type check 20 | @deno test --unstable --no-run ${TARGETS} 21 | 22 | test: FORCE ## Test 23 | @deno test --unstable -A --no-check --jobs 24 | 25 | deps: FORCE ## Update dependencies 26 | @deno run -A https://deno.land/x/udd@0.8.1/main.ts ${TARGETS} 27 | @make fmt 28 | 29 | FORCE: 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddc-dictionary 2 | 3 | ddc source for dictionary 4 | 5 | ## Required 6 | 7 | ### denops.vim 8 | 9 | 10 | 11 | ### ddc.vim 12 | 13 | 14 | 15 | ## Configuration examples 16 | 17 | For detail, please see help file. 18 | 19 | ```vim 20 | " you need to set 'dictionary' option 21 | setlocal dictionary+=/usr/share/dict/words 22 | " or you can specify dictionary path using sourceParams ('dictPaths' must be list of files) 23 | call ddc#custom#patch_global('sourceParams', { 24 | \ 'dictionary': {'dictPaths': 25 | \ ['/usr/share/dict/german', 26 | \ '/usr/share/dict/words', 27 | \ '/usr/share/dict/spanish'], 28 | \ 'smartCase': v:true, 29 | \ 'isVolatile': v:true, 30 | \ } 31 | \ }) 32 | 33 | call ddc#custom#patch_global('sources', ['dictionary']) 34 | call ddc#custom#patch_global('sourceOptions', { 35 | \ '_': {'matchers': ['matcher_head']}, 36 | \ 'dictionary': {'mark': 'D'}, 37 | \ }) 38 | ``` 39 | 40 | ## Original version 41 | 42 | 43 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "exclude": [ 4 | "docs/**", 5 | ".coverage/**" 6 | ], 7 | "tasks": { 8 | "check": "deno check ./**/*.ts", 9 | "test": "deno test -A --shuffle --doc", 10 | "test:coverage": "deno task test --coverage=.coverage", 11 | "coverage": "deno coverage .coverage", 12 | "update": "deno run --allow-env --allow-read --allow-write --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", 13 | "update:write": "deno task -q update --write", 14 | "update:commit": "deno task -q update --commit --prefix :package: --pre-commit=fmt,lint" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /denops/@ddc-sources/dictionary.ts: -------------------------------------------------------------------------------- 1 | import type { DdcEvent, Item } from "jsr:@shougo/ddc-vim@7.1.0/types"; 2 | import { 3 | BaseSource, 4 | type GatherArguments, 5 | type OnEventArguments, 6 | } from "jsr:@shougo/ddc-vim@7.1.0/source"; 7 | import * as fn from "jsr:@denops/std@7.2.0/function"; 8 | import { assertEquals } from "jsr:@std/assert@1.0.6"; 9 | 10 | type DictCache = { 11 | mtime: Date | null; 12 | candidates: Item[]; 13 | }; 14 | 15 | export function isUpper(char: string) { 16 | return /[A-Z]/.test(char[0]); 17 | } 18 | 19 | function extractLastWord( 20 | str: string, 21 | ): [string, number] { 22 | if (str.match(/[^a-zA-Z]$/)) { 23 | return ["", str.length]; 24 | } 25 | const upperCaseRegexp = /[A-Z][A-Z]+$/; 26 | const camelCaseRegexp = /[A-Z][a-z]*$/; // Also matched to PascalCase 27 | const snakeCaseRegexp = /[a-z][a-z]*$/; // Also matched to kebab-case, etc. 28 | let matches: string[] | null = str.match(upperCaseRegexp); 29 | if (matches === null) matches = str.match(camelCaseRegexp); 30 | if (matches === null) matches = str.match(snakeCaseRegexp); 31 | if (matches === null) return [str, 0]; 32 | const lastWord = matches.at(-1) || str; 33 | const offset = str.lastIndexOf(lastWord); 34 | return [lastWord, offset]; 35 | } 36 | 37 | type Params = { 38 | dictPaths: string[]; 39 | smartcase: boolean; 40 | showMenu: boolean; 41 | }; 42 | 43 | export class Source extends BaseSource { 44 | private cache: { [filename: string]: DictCache } = {}; 45 | private dicts: string[] = []; 46 | private lastPrecedingLetters: string | undefined = undefined; 47 | private lastItems: Item[] | undefined = undefined; 48 | private simplestItems: Item[] | undefined = undefined; 49 | events = ["InsertEnter"] as DdcEvent[]; 50 | 51 | private getDictionaries(dictOpt: string): string[] { 52 | if (dictOpt) { 53 | return dictOpt.split(","); 54 | } else { 55 | return []; 56 | } 57 | } 58 | 59 | private makeCache(): void { 60 | if (!this.dicts) { 61 | return; 62 | } 63 | 64 | for (const dictFile of this.dicts) { 65 | const mtime = Deno.statSync(dictFile).mtime; 66 | if ( 67 | dictFile in this.cache && 68 | this.cache[dictFile].mtime?.getTime() == mtime?.getTime() 69 | ) { 70 | return; 71 | } 72 | const texts = Deno.readTextFileSync(dictFile).split(/\r?\n/); 73 | this.cache[dictFile] = { 74 | "mtime": mtime, 75 | "candidates": texts.map((word) => ({ word, menu: dictFile })), 76 | }; 77 | } 78 | } 79 | 80 | async onInit( 81 | args: GatherArguments, 82 | ): Promise { 83 | await this.onEvent(args); 84 | } 85 | 86 | async onEvent({ 87 | denops, 88 | sourceParams, 89 | }: OnEventArguments): Promise { 90 | this.dicts = this.getDictionaries( 91 | await fn.getbufvar(denops, 1, "&dictionary") as string, 92 | ); 93 | const paths = sourceParams.dictPaths; 94 | if (paths && Array.isArray(paths)) { 95 | this.dicts = this.dicts.concat(paths as string[]); 96 | } 97 | this.makeCache(); 98 | } 99 | 100 | gather({ 101 | completeStr, 102 | sourceParams, 103 | }: GatherArguments): Promise { 104 | if (!this.dicts) { 105 | return Promise.resolve([]); 106 | } 107 | 108 | const [lastWord, offset] = extractLastWord(completeStr); 109 | // Note: The early returns only makes sense when the option `isVolatile` is 110 | // set to true. 111 | if (offset === 0 && this.simplestItems) { 112 | return Promise.resolve(this.simplestItems); 113 | } 114 | const precedingLetters = completeStr.slice(0, offset); 115 | if ( 116 | this.lastItems && 117 | this.lastPrecedingLetters === precedingLetters 118 | ) { 119 | return Promise.resolve(this.lastItems); 120 | } 121 | this.lastPrecedingLetters = precedingLetters; 122 | const isFirstUpper = lastWord.length ? isUpper(lastWord[0]) : false; 123 | const isSecondUpper = lastWord.length > 1 ? isUpper(lastWord[1]) : false; 124 | return Promise.resolve((() => { 125 | const items = this.dicts.map((dict) => this.cache[dict].candidates) 126 | .flatMap((candidates) => candidates) 127 | .map((candidate) => { 128 | let word = candidate.word; 129 | if (sourceParams.smartcase) { 130 | if (isSecondUpper) return { word: candidate.word.toUpperCase() }; 131 | if (isFirstUpper) { 132 | word = candidate.word.replace(/^./, (m) => m.toUpperCase()); 133 | } 134 | } 135 | return { 136 | word: word, 137 | menu: sourceParams.showMenu ? candidate.menu : "", 138 | }; 139 | }) 140 | .map((candidate) => { 141 | candidate.word = precedingLetters.concat(candidate.word); 142 | return candidate; 143 | }); 144 | if (offset > 0) { 145 | this.lastItems = items; 146 | } else if (this.simplestItems === undefined) { 147 | this.simplestItems = items; 148 | } 149 | return items; 150 | })()); 151 | } 152 | 153 | params(): Params { 154 | return { 155 | dictPaths: [], 156 | smartcase: true, 157 | showMenu: true, 158 | }; 159 | } 160 | } 161 | 162 | Deno.test("extractLastWord", () => { 163 | assertEquals( 164 | extractLastWord("input"), 165 | ["input", 0], 166 | ); 167 | assertEquals( 168 | extractLastWord("UPPER_CASE_INPUT"), 169 | ["INPUT", 11], 170 | ); 171 | assertEquals( 172 | extractLastWord("camelCaseInput"), 173 | ["Input", 9], 174 | ); 175 | assertEquals( 176 | extractLastWord("_snake_case_input"), 177 | ["input", 12], 178 | ); 179 | assertEquals( 180 | extractLastWord("_unfinished_input_"), 181 | ["", 18], 182 | ); 183 | assertEquals( 184 | extractLastWord("unfinishedI"), 185 | ["I", 10], 186 | ); 187 | assertEquals( 188 | extractLastWord("_i"), 189 | ["i", 1], 190 | ); 191 | }); 192 | -------------------------------------------------------------------------------- /doc/ddc-dictionary.txt: -------------------------------------------------------------------------------- 1 | *ddc-dictionary.txt* dictionary source for ddc.vim 2 | 3 | Author: matsui54 4 | License: MIT license 5 | 6 | CONTENTS *ddc-dictionary-contents* 7 | 8 | Introduction |ddc-dictionary-introduction| 9 | Install |ddc-dictionary-install| 10 | Usage |ddc-dictionary-usage| 11 | Examples |ddc-dictionary-examples| 12 | Params |ddc-dictionary-params| 13 | 14 | 15 | ============================================================================== 16 | INTRODUCTION *ddc-dictionary-introduction* 17 | 18 | This source collects words from dictionary files specified by 'dictionary' or 19 | params. 20 | 21 | ============================================================================== 22 | INSTALL *ddc-dictionary-install* 23 | 24 | Please install both "ddc.vim" and "denops.vim". 25 | 26 | https://github.com/Shougo/ddc.vim 27 | https://github.com/vim-denops/denops.vim 28 | 29 | ============================================================================== 30 | USAGE *ddc-dictionary-usage* 31 | 32 | Set 'dictionary' option or set "dictionary" sourceParams like example below. 33 | 34 | ============================================================================== 35 | EXAMPLES *ddc-dictionary-examples* 36 | 37 | > 38 | " you need to set 'dictionary' option 39 | setlocal dictionary+=/usr/share/dict/words 40 | " or you can specify dictionary path using sourceParams 41 | " ('dictPaths' must be list of files) 42 | call ddc#custom#patch_global('sourceParams', { 43 | \ 'dictionary': {'dictPaths': 44 | \ ['/usr/share/dict/german', 45 | \ '/usr/share/dict/words', 46 | \ '/usr/share/dict/spanish'], 47 | \ 'smartCase': v:true, 48 | \ 'isVolatile': v:true, 49 | \ } 50 | \ }) 51 | 52 | call ddc#custom#patch_global('sources', ['dictionary']) 53 | call ddc#custom#patch_global('sourceOptions', { 54 | \ '_': {'matchers': ['matcher_head']}, 55 | \ 'dictionary': {'mark': 'D'}, 56 | \ }) 57 | < 58 | 59 | ============================================================================== 60 | PARAMS *ddc-dictionary-params* 61 | 62 | *ddc-dictionary-param-dictPaths* 63 | dictPaths (string[]) 64 | List of dictionary paths. Dictionary file must have the same 65 | format as the one specified by 'dictionary'. 66 | 67 | Default: [] 68 | 69 | *ddc-dictionary-param-smartCase* 70 | smartCase (boolean) 71 | If it is true, words are converted to uppercase depending on 72 | user input. When only first character of input is uppercase, 73 | candidates' first character becomes uppercase. When first and 74 | second characters are both uppercase, the entire candidates' 75 | become uppercase. 76 | 77 | Default: v:true 78 | *ddc-dictionary-param-showMenu* 79 | showMenu (boolean) 80 | If it is true, path of dictionary is shown in menu. 81 | 82 | Default: v:true 83 | 84 | ============================================================================== 85 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 86 | --------------------------------------------------------------------------------