├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── README.md ├── extension ├── README.md └── src │ ├── bcp.png │ ├── content.js │ ├── manifest.json │ ├── popup.html │ └── popup.js ├── package.json ├── scripts ├── gen_package_type.js ├── gendoc.ts └── readme.md ├── src ├── completion.ts ├── dispose.ts ├── index.ts ├── server.ts ├── types │ └── pkg-config.d.ts └── util.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | }, 11 | plugins: ['@typescript-eslint'], 12 | rules: { 13 | '@typescript-eslint/explicit-function-return-type': 'off', 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | '@typescript-eslint/no-empty-function': 'off', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: push 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup Node 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - name: Build Package 16 | run: | 17 | npm install -g json ts-node 18 | npm install 19 | - name: Publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | run: | 23 | pkgname=$(json name < package.json) 24 | localversion=$(json version < package.json) 25 | remoteversion=$(npm view ${pkgname} version) 26 | 27 | echo ${localversion} 28 | echo ${remoteversion} 29 | 30 | if [ ${localversion} \> ${remoteversion} ] 31 | then 32 | npm publish 33 | git config user.name github-actions 34 | git config user.email github-actions@github.com 35 | git tag -a ${localversion} -m v${localversion} 36 | git push --tags 37 | fi 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # For current directory only 2 | # ---------------------------------------------------------------------------- 3 | /source 4 | /lib 5 | /package-lock.json 6 | # General 7 | # ---------------------------------------------------------------------------- 8 | *.o 9 | *.out 10 | 11 | # log 12 | *.log 13 | 14 | # cache 15 | *.cache 16 | cache/ 17 | 18 | # Windows 19 | # ---------------------------------------------------------------------------- 20 | Thumbs.db 21 | Desktop.ini 22 | 23 | # Tags 24 | # ----------------------------------------------------------------------------- 25 | TAGS 26 | !TAGS/ 27 | tags 28 | tags-cn 29 | !tags/ 30 | .tags 31 | .tags1 32 | tags.lock 33 | tags.temp 34 | gtags.files 35 | GTAGS 36 | GRTAGS 37 | GPATH 38 | cscope.files 39 | cscope.out 40 | cscope.in.out 41 | cscope.po.out 42 | 43 | # Vim 44 | # ------------------------------------------------------------------------------ 45 | [._]*.s[a-w][a-z] 46 | [._]s[a-w][a-z] 47 | *.un~ 48 | Session.vim 49 | .netrwhist 50 | *~ 51 | 52 | # Test % Tmp 53 | # ------------------------------------------------------------------------------- 54 | test.* 55 | tmp.* 56 | temp.* 57 | 58 | # Java 59 | # ------------------------------------------------------------------------------- 60 | *.class 61 | 62 | # JavaScript 63 | # ------------------------------------------------------------------------------- 64 | node_modules 65 | 66 | # Python 67 | # ------------------------------------------------------------------------------- 68 | *.pyc 69 | .idea/ 70 | /.idea 71 | build/ 72 | __pycache__ 73 | 74 | # Rust 75 | # ------------------------------------------------------------------------------- 76 | target/ 77 | **/*.rs.bk 78 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | extension 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coc-browser 2 | 3 | ![publish](https://github.com/voldikss/coc-browser/workflows/publish/badge.svg) 4 | [![npm version](https://badge.fury.io/js/coc-browser.svg)](https://badge.fury.io/js/coc-browser) 5 | 6 | Browser words completion source for [coc.nvim](https://github.com/neoclide/coc.nvim) 7 | 8 | ![](https://user-images.githubusercontent.com/20282795/103974806-88212e00-51ad-11eb-9b22-61f230c2ab9e.gif) 9 | 10 | ## Installation 11 | 12 | - **Install the [browser extension](https://chrome.google.com/webstore/detail/browser-source-provider/lkaldcfmhailjfcbapicgkdkkamanlml?utm_source=chrome-ntp-icon)** 13 | 14 | Browser extension is used to grab words from web page and send them to the local server 15 | 16 | - **Install [coc.nvim](https://github.com/neoclide/coc.nvim)** 17 | 18 | - **Install coc-browser** 19 | 20 | ```vim 21 | :CocInstall coc-browser 22 | ``` 23 | 24 | ## Config 25 | 26 | - `browser.shortcut`: 27 | default: `"web"` 28 | 29 | - `browser.priority`: 30 | default: `5` 31 | 32 | - `browser.patterns`: default: `{"*": []}` 33 | 34 | Javascript style regex patterns that defines the cursor position to enable autocomplete, empty array `[]` means to enable for whole buffer. 35 | 36 | For example, in order to enable completion only if the cursor is in the 37 | comment region in javascript file, set this option as follows 38 | 39 | ```jsonc 40 | "browser.patterns": { 41 | "javascript": [ 42 | "^\\s*\\/\\/", 43 | "^\\s*\\/\\*", 44 | "^\\s*\\*" 45 | ] 46 | } 47 | ``` 48 | 49 | The `*` in the default value `{"*": []}` means to enable autocomplete for all 50 | filetypes. 51 | 52 | - `browser.port`: 53 | default: `8888` 54 | 55 | Port used to transfer words from browser extension to local server 56 | 57 | ## Command 58 | 59 | - `:CocCommand browser.clearCache`: Clear completion source cache 60 | 61 | ## License 62 | 63 | MIT 64 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # browser-source-provider 2 | 3 | Browser word completion source extension, developed for [coc-browser](https://github.com/voldikss/coc-browser) and [vscode-browser-completion](https://github.com/voldikss/vscode-browser-completion) 4 | 5 | ## Install 6 | 7 | - For Google Chrome: [link](https://chrome.google.com/webstore/detail/browser-source-provider/lkaldcfmhailjfcbapicgkdkkamanlml) 8 | 9 | - For Firefox: [link](https://addons.mozilla.org/firefox/addon/voldikss/) 10 | 11 | ## Reference 12 | 13 | - [coc-browser](https://github.com/voldikss/coc-browser) 14 | - [vscode-browser-completion](https://github.com/voldikss/vscode-browser-completion) 15 | -------------------------------------------------------------------------------- /extension/src/bcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voldikss/coc-browser/5cd2c3f5b96a050ba42a7189dc267f83a3565e4b/extension/src/bcp.png -------------------------------------------------------------------------------- /extension/src/content.js: -------------------------------------------------------------------------------- 1 | chrome.storage.sync.get(['port'], (items) => { 2 | let port = 8888 3 | if (Object.keys(items).length != 0) { 4 | port = parseInt(items.port) 5 | } 6 | 7 | const xhr = new XMLHttpRequest() 8 | xhr.responseType = 'text' 9 | xhr.open('POST', `http://127.0.0.1:${port}`) 10 | xhr.onload = () => { 11 | if (xhr.readyState == 4 && xhr.status == 200) { 12 | // console.log('send words to coc-browser successfully') 13 | // console.log(xhr.responseText) 14 | } else { 15 | // console.log("failed to send to coc-browser") 16 | } 17 | } 18 | let text = document.body.innerText.match(/[0-9a-zA-Z_]{5,20}/g) 19 | if (text) { 20 | text = [...new Set(text)].join('\n') 21 | xhr.send(text) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /extension/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "browser-source-provider", 4 | "version": "1.2.0", 5 | "description": "browser words completion source for coc-browser", 6 | "author": "voldikss", 7 | "icons": { 8 | "16": "bcp.png", 9 | "48": "bcp.png", 10 | "128": "bcp.png" 11 | }, 12 | "browser_action": { 13 | "default_popup": "popup.html", 14 | "default_title": "browser-source-provider" 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [""], 19 | "js": ["content.js"], 20 | "all_frames": true 21 | } 22 | ], 23 | "permissions": ["https://127.0.0.1/*", "http://127.0.0.1/*", "storage"] 24 | } 25 | -------------------------------------------------------------------------------- /extension/src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 19 |
13 | 14 | 16 | 17 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /extension/src/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | chrome.storage.sync.get(['port'], (items) => { 3 | let port = 8888 4 | if (Object.keys(items).length != 0) { 5 | port = parseInt(items.port) 6 | } 7 | document.getElementById('port').value = port 8 | }) 9 | }) 10 | 11 | document.getElementById('save').addEventListener('click', () => { 12 | const port = document.getElementById('port').value 13 | chrome.storage.sync.set({port: port}, () => { 14 | window.close() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-browser", 3 | "version": "1.5.0", 4 | "description": "browser words completion for coc.nvim", 5 | "main": "lib/index.js", 6 | "engines": { 7 | "coc": "^0.0.74" 8 | }, 9 | "keywords": [ 10 | "coc.nvim", 11 | "completion", 12 | "neovim", 13 | "vim" 14 | ], 15 | "scripts": { 16 | "clean": "rimraf lib", 17 | "watch": "webpack --watch", 18 | "build:types": "node scripts/gen_package_type.js", 19 | "build:webpack": "webpack --mode production", 20 | "build:doc": "ts-node ./scripts/gendoc.ts", 21 | "build": "run-s build:types build:webpack build:doc", 22 | "prepare": "run-s clean build", 23 | "lint": "eslint . --ext .ts" 24 | }, 25 | "activationEvents": [ 26 | "*" 27 | ], 28 | "repository": "https://github.com/voldikss/coc-browser", 29 | "homepage": "https://github.com/voldikss/coc-browser/#readme", 30 | "contributes": { 31 | "configuration": { 32 | "title": "Browser", 33 | "type": "object", 34 | "properties": { 35 | "browser.shortcut": { 36 | "type": "string", 37 | "default": "WEB" 38 | }, 39 | "browser.priority": { 40 | "type": "number", 41 | "default": 5 42 | }, 43 | "browser.patterns": { 44 | "type": "object", 45 | "default": { 46 | "*": [] 47 | } 48 | }, 49 | "browser.port": { 50 | "type": "number", 51 | "default": 8888, 52 | "description": "Port used to transfer words from browser extension to local server" 53 | } 54 | } 55 | }, 56 | "commands": [ 57 | { 58 | "title": "Clear browser completion source cache", 59 | "command": "clearCache" 60 | } 61 | ] 62 | }, 63 | "author": "dyzplus@gmail.com", 64 | "license": "MIT", 65 | "devDependencies": { 66 | "@types/chrome": "0.0.126", 67 | "@types/node": "^14.14.16", 68 | "@typescript-eslint/eslint-plugin": "^4.11.1", 69 | "@typescript-eslint/parser": "^4.11.1", 70 | "@voldikss/tsconfig": "*", 71 | "coc.nvim": "^0.0.80", 72 | "eslint": "^7.16.0", 73 | "rimraf": "^3.0.2", 74 | "ts-loader": "^8.0.12", 75 | "typescript": "^4.1.3", 76 | "json-schema-to-typescript": "^10.1.2", 77 | "npm-run-all": "^4.1.5", 78 | "webpack": "^5.11.1", 79 | "webpack-cli": "^4.3.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/gen_package_type.js: -------------------------------------------------------------------------------- 1 | const Pkg = require('../package.json'); 2 | const fs = require('fs'); 3 | const { compile } = require('json-schema-to-typescript'); 4 | 5 | const fsp = fs.promises; 6 | 7 | async function main() { 8 | const s = await compile(Pkg.contributes.configuration, 'Extension', { 9 | style: { 10 | semi: true, 11 | singleQuote: true, 12 | }, 13 | }); 14 | await fsp.writeFile('src/types/pkg-config.d.ts', s); 15 | } 16 | 17 | main().then(console.error); 18 | 19 | -------------------------------------------------------------------------------- /scripts/gendoc.ts: -------------------------------------------------------------------------------- 1 | import Pkg from '../package.json'; 2 | import fs from 'fs'; 3 | import { JSONSchema7, JSONSchema7Type } from 'json-schema'; 4 | import ts from 'typescript'; 5 | import pathLib from 'path'; 6 | 7 | const fsp = fs.promises; 8 | 9 | type Definition = JSONSchema7; 10 | 11 | type Cmd = { 12 | title: string; 13 | command: string; 14 | }; 15 | 16 | type Section = { 17 | title?: string; 18 | rows: Row[]; 19 | }; 20 | 21 | type Row = { 22 | name: string; 23 | description: string; 24 | type?: string; 25 | default?: JSONSchema7Type; 26 | }; 27 | 28 | abstract class DocGenerator { 29 | protected ignorePrettierStart = ''; 30 | protected ignorePrettierEnd = ''; 31 | protected hint = ``; 32 | 33 | constructor(public generateCommand: string) {} 34 | 35 | abstract generate(): Promise; 36 | 37 | protected printJson(obj: JSONSchema7Type, format = false) { 38 | return JSON.stringify(obj, undefined, format ? ' ' : undefined); 39 | } 40 | 41 | protected printAsDetails(rows: Row[]) { 42 | const lines: string[] = []; 43 | rows.forEach((row) => { 44 | let hideLine = ''; 45 | if (row.type) { 46 | hideLine += `Type:
${row.type}
`; 47 | } 48 | if (row.default !== undefined) { 49 | hideLine += 'Default: '; 50 | hideLine += 51 | '
' + this.printJson(row.default, true) + '
'; 52 | } 53 | if (hideLine) { 54 | lines.push(`
`); 55 | } 56 | lines.push( 57 | `${row.name}: ${row.description}.`, 58 | ); 59 | if (hideLine) { 60 | lines.push(hideLine); 61 | lines.push('
'); 62 | } 63 | }); 64 | return lines; 65 | } 66 | 67 | /** 68 | * @deprecated 69 | */ 70 | protected printAsList(rows: Row[]) { 71 | const lines: string[] = []; 72 | rows.forEach((row) => { 73 | let line = `- \`${row.name}\``; 74 | const descriptions: string[] = []; 75 | if (row.description) { 76 | descriptions.push(row.description); 77 | } 78 | if (row.type) { 79 | descriptions.push(`type: \`${this.printJson(row.type)}\``); 80 | } 81 | if (row.default !== undefined) { 82 | descriptions.push(`default: \`${this.printJson(row.default)}\``); 83 | } 84 | if (descriptions.length) { 85 | line += ': ' + descriptions.join(', '); 86 | } 87 | lines.push(line); 88 | }); 89 | return lines; 90 | } 91 | 92 | async attach(headLevel: number, attachTitle: string, markdownPath: string) { 93 | const markdown = await fsp.readFile(markdownPath, 'utf8'); 94 | const markdownLines = markdown.split('\n'); 95 | let startIndex = markdownLines.findIndex((line) => 96 | new RegExp('#'.repeat(headLevel) + '\\s*' + attachTitle + '\\s*').test( 97 | line, 98 | ), 99 | ); 100 | if (startIndex < 0) { 101 | return; 102 | } 103 | startIndex += 1; 104 | const endIndex = markdownLines 105 | .slice(startIndex) 106 | .findIndex((line) => new RegExp(`#{1,${headLevel}}[^#]`).test(line)); 107 | const removeCount = endIndex < 0 ? 0 : endIndex; 108 | 109 | const sections = await this.generate(); 110 | const lines: string[] = ['', this.hint, this.ignorePrettierStart]; 111 | for (const section of sections) { 112 | if (section.title) { 113 | lines.push(`${section.title}`); 114 | } 115 | lines.push(...this.printAsDetails(section.rows)); 116 | } 117 | lines.push(''); 118 | lines.push(this.ignorePrettierEnd); 119 | lines.push(''); 120 | markdownLines.splice(startIndex, removeCount, ...lines); 121 | console.log(markdownLines.join('\n')) 122 | await fsp.writeFile(markdownPath, markdownLines.join('\n')); 123 | console.log(`Attached to ${attachTitle} header`); 124 | } 125 | } 126 | 127 | class ConfigurationDocGenerator extends DocGenerator { 128 | constructor( 129 | generateCommand: string, 130 | public packageDeclarationFilepath: string, 131 | ) { 132 | super(generateCommand); 133 | } 134 | 135 | isNodeExported(node: ts.Node) { 136 | return ( 137 | (ts.getCombinedModifierFlags(node as ts.Declaration) & 138 | ts.ModifierFlags.Export) !== 139 | 0 || 140 | (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) 141 | ); 142 | } 143 | 144 | async generate() { 145 | const defRows: Row[] = []; 146 | const propRows: Row[] = []; 147 | 148 | const conf = Pkg.contributes.configuration; 149 | const title = conf.title; 150 | const filename = pathLib.basename(this.packageDeclarationFilepath); 151 | 152 | const Kind = ts.SyntaxKind; 153 | const prog = ts.createProgram([this.packageDeclarationFilepath], { 154 | strict: true, 155 | }); 156 | const sourceFile = prog.getSourceFile(this.packageDeclarationFilepath)!; 157 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 158 | const checker = prog.getTypeChecker(); 159 | 160 | function print(node: ts.Node): string { 161 | return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); 162 | } 163 | 164 | function debug(node: ts.Node) { 165 | console.log(Kind[node.kind]); 166 | console.log(print(node)); 167 | } 168 | 169 | sourceFile.forEachChild((node) => { 170 | if (!this.isNodeExported(node)) { 171 | return; 172 | } 173 | 174 | if (ts.isTypeAliasDeclaration(node)) { 175 | defRows.push({ 176 | name: node.name.text, 177 | description: node.name.text, 178 | type: print(node.type), 179 | }); 180 | } else if (ts.isInterfaceDeclaration(node)) { 181 | if (node.name.text === title) { 182 | node.forEachChild((prop) => { 183 | if (!ts.isPropertySignature(prop)) { 184 | return; 185 | } 186 | const symbol = checker.getSymbolAtLocation(prop.name); 187 | if (!symbol) { 188 | return; 189 | } 190 | 191 | const name = symbol.getName(); 192 | // @ts-ignore 193 | const jsonProp = conf.properties[name as any] as Definition & { 194 | default_doc?: string; 195 | }; 196 | propRows.push({ 197 | name, 198 | description: ts.displayPartsToString( 199 | symbol.getDocumentationComment(checker), 200 | ), 201 | type: prop.type ? print(prop.type) : undefined, 202 | default: jsonProp.default_doc 203 | ? jsonProp.default_doc 204 | : jsonProp.default, 205 | }); 206 | }); 207 | } 208 | } else { 209 | console.error(`[gen_doc] ${filename} not support ${print(node)}`); 210 | } 211 | }); 212 | 213 | return [ 214 | { title: 'Properties', rows: propRows }, 215 | ]; 216 | } 217 | } 218 | 219 | // class CommandDocGenerator extends DocGenerator { 220 | // async generate() { 221 | // const cmds = Pkg.contributes.commands as Cmd[]; 222 | // const rows: Row[] = []; 223 | // cmds.forEach((cmd) => { 224 | // rows.push({ 225 | // name: cmd.command, 226 | // description: cmd.title, 227 | // }); 228 | // }); 229 | // return [{ rows }]; 230 | // } 231 | // } 232 | 233 | async function main() { 234 | const cmd = 'yarn run bulid:doc'; 235 | const markdownPath = `${__dirname}/../README.md` 236 | const packageDeclarationFilepath = `${__dirname}/../src/types/pkg-config.d.ts` 237 | // await new CommandDocGenerator(cmd).attach(2, 'Commands', markdownPath); 238 | await new ConfigurationDocGenerator(cmd, packageDeclarationFilepath).attach( 239 | 2, 240 | 'Configuration', 241 | markdownPath 242 | ); 243 | } 244 | 245 | main().catch(console.error); 246 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | Reference: https://github.com/weirongxu/coc-explorer/tree/master/scripts 2 | -------------------------------------------------------------------------------- /src/completion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | CompletionItemKind, 4 | CompletionItemProvider, 5 | Position, 6 | ProviderResult, 7 | Range, 8 | TextDocument, 9 | workspace, 10 | } from 'coc.nvim' 11 | import { fsReadFile, fsReadDir, fsRmFile } from './util' 12 | import path from 'path' 13 | import Server from './server' 14 | 15 | export class BrowserCompletionProvider implements CompletionItemProvider { 16 | private sourceDir: string 17 | constructor( 18 | server: Server, 19 | private patterns: Record 20 | ) { this.sourceDir = server.cacheDir } 21 | 22 | provideCompletionItems(document: TextDocument, position: Position): ProviderResult { 23 | const { languageId, uri } = document 24 | 25 | const patterns = this.patterns['*'] || this.patterns[languageId] 26 | if (!patterns) return [] 27 | 28 | const doc = workspace.getDocument(uri) 29 | if (!doc) return [] 30 | 31 | const wordRange = doc.getWordRangeAtPosition(Position.create(position.line, position.character - 1)) 32 | if (!wordRange) return [] 33 | 34 | const word = document.getText(wordRange) 35 | const linePre = document.getText(Range.create(Position.create(position.line, 0), position)) 36 | if (!patterns.length || patterns.some(p => new RegExp(p).test(linePre))) { 37 | return this.gatherCandidates(word) 38 | } 39 | return [] 40 | } 41 | 42 | private async gatherCandidates(word): Promise { 43 | const files = await fsReadDir(this.sourceDir) 44 | return new Promise((resolve, reject) => { 45 | Promise.all(files.map(f => { 46 | const sourcePath = path.join(this.sourceDir, f) 47 | return fsReadFile(sourcePath) 48 | .then(content => content 49 | .split(/\n/)) 50 | .catch(e => reject(e)) 51 | })) 52 | .then(results => { 53 | const words: string[] = Array.prototype.concat.apply([], results) 54 | const candidates = [...new Set(words)] 55 | .filter(w => new RegExp(word).test(w)) 56 | .map(word => ({ 57 | label: word, 58 | kind: CompletionItemKind.Text, 59 | insertText: word, 60 | })) 61 | resolve(candidates) 62 | }) 63 | .catch(e => reject(e)) 64 | }) 65 | } 66 | 67 | public async clearCache(): Promise { 68 | const sourceFiles = await fsReadDir(this.sourceDir) 69 | for (const file of sourceFiles) { 70 | const filepath = path.join(this.sourceDir, file) 71 | await fsRmFile(filepath) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/dispose.ts: -------------------------------------------------------------------------------- 1 | // from https://github.com/iamcco/coc-zi/blob/master/src/common/dispose.ts 2 | import { Disposable } from 'coc.nvim' 3 | 4 | export class Dispose implements Disposable { 5 | private subscriptions: Disposable[] = [] 6 | 7 | private push(subs: Disposable): void { 8 | this.subscriptions.push(subs) 9 | } 10 | 11 | public dispose(): void { 12 | if (this.subscriptions.length > 0) { 13 | this.subscriptions.forEach(subs => { 14 | subs.dispose() 15 | }) 16 | this.subscriptions = [] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, languages, workspace, commands } from 'coc.nvim' 2 | import { BrowserCompletionProvider } from './completion' 3 | import { fsStat, fsMkdir } from './util' 4 | import Server from './server' 5 | 6 | export async function activate(context: ExtensionContext): Promise { 7 | const { subscriptions, storagePath } = context 8 | const stat = await fsStat(storagePath) 9 | if (!(stat?.isDirectory())) { 10 | await fsMkdir(storagePath) 11 | } 12 | 13 | const config = workspace.getConfiguration('browser') 14 | 15 | const server = new Server( 16 | config.get('port'), 17 | storagePath 18 | ) 19 | await server.start() 20 | subscriptions.push(server) 21 | 22 | const browserCompletionProvider = new BrowserCompletionProvider( 23 | server, 24 | config.get>('patterns') 25 | ) 26 | 27 | subscriptions.push( 28 | commands.registerCommand( 29 | 'browser.clearCache', 30 | async () => { 31 | await browserCompletionProvider.clearCache() 32 | } 33 | ) 34 | ) 35 | 36 | subscriptions.push( 37 | languages.registerCompletionItemProvider( 38 | 'coc-browser', 39 | config.get('shortcut'), 40 | null, 41 | browserCompletionProvider, 42 | [], 43 | config.get('priority'), 44 | [], 45 | ) 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import path from 'path' 3 | import { window } from 'coc.nvim' 4 | import { fsWriteFile } from './util' 5 | import { Dispose } from './dispose' 6 | 7 | export default class Server extends Dispose { 8 | private counter = 0 9 | private server: http.Server 10 | private capacity = 8 // 8 cache files, so 8 webpages at most 11 | constructor(private port: number, public cacheDir: string) { 12 | super() 13 | } 14 | 15 | public async start(): Promise { 16 | this.server = new http.Server() 17 | this.server.listen(this.port) 18 | // if there is already a server running on the port 19 | // then close this server 20 | this.server.once('error', () => { 21 | this.server.close() 22 | }) 23 | this.server.once('listening', () => { 24 | // todo: write to CocLog 25 | }) 26 | let words = '' 27 | this.server.on('request', (request, response) => { 28 | request.on('data', data => { 29 | words += data 30 | }) 31 | request.on('end', async () => { 32 | await this.saveWords(words) 33 | words = '' // NOTE: Important! OMG! 34 | }) 35 | request.on('error', e => { 36 | window.showMessage(`request error from browser: ${e.message}`) 37 | }) 38 | response.writeHead(200, { 'Content-Type': 'text/plain' }) 39 | response.write('response from coc-browser local server\n') 40 | response.end() 41 | }) 42 | } 43 | 44 | public dispose(): void { 45 | this.server.close(() => { 46 | // nop 47 | }) 48 | } 49 | 50 | public async saveWords(text: string): Promise { 51 | const { cacheDir } = this 52 | const cachePath = path.join(cacheDir, `${this.counter % this.capacity}`) 53 | await fsWriteFile(cachePath, text) 54 | this.counter++ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/types/pkg-config.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /** 3 | * This file was automatically generated by json-schema-to-typescript. 4 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 5 | * and run json-schema-to-typescript to regenerate this file. 6 | */ 7 | 8 | export interface Browser { 9 | 'browser.shortcut'?: string; 10 | 'browser.priority'?: number; 11 | 'browser.patterns'?: { 12 | [k: string]: unknown; 13 | }; 14 | /** 15 | * Port used to transfer words from browser extension to local server 16 | */ 17 | 'browser.port'?: number; 18 | [k: string]: unknown; 19 | } 20 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | export async function fsStat(filepath: string): Promise { 4 | return new Promise(resolve => { 5 | fs.stat(filepath, (err, stats) => { 6 | if (err) resolve(null) 7 | resolve(stats) 8 | }) 9 | }) 10 | } 11 | 12 | export async function fsRmFile(fullpath: string): Promise { 13 | return new Promise((resolve, reject) => { 14 | fs.unlink(fullpath, err => { 15 | if (err) reject() 16 | resolve() 17 | }) 18 | }) 19 | } 20 | 21 | export async function fsWriteFile(fullpath: string, content: string): Promise { 22 | return new Promise((resolve, reject) => { 23 | fs.writeFile(fullpath, content, 'utf8', err => { 24 | if (err) reject() 25 | resolve() 26 | }) 27 | }) 28 | } 29 | 30 | export function fsReadFile(fullpath: string, encoding = 'utf8'): Promise { 31 | return new Promise((resolve, reject) => { 32 | fs.readFile(fullpath, encoding, (err, content) => { 33 | if (err) reject(err) 34 | resolve(content) 35 | }) 36 | }) 37 | } 38 | 39 | export function fsReadDir(fullpath: string): Promise { 40 | return new Promise((resolve, reject) => { 41 | fs.readdir(fullpath, (err, files) => { 42 | if (err) reject(err) 43 | resolve(files) 44 | }) 45 | }) 46 | } 47 | 48 | export function fsMkdir(filepath: string): Promise { 49 | return new Promise((resolve, reject) => { 50 | fs.mkdir(filepath, err => { 51 | if (err) reject(err) 52 | resolve() 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@voldikss/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "lib", 6 | "target": "es2015", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "lib": ["es2018"], 10 | "plugins": [] 11 | }, 12 | "include": ["src"], 13 | "exclude": [] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | target: 'node', 6 | mode: 'none', 7 | resolve: { 8 | mainFields: ['module', 'main'], 9 | extensions: ['.js', '.ts'] 10 | }, 11 | externals: { 12 | 'coc.nvim': 'commonjs coc.nvim' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.ts$/, 18 | exclude: /node_modules/, 19 | use: [ 20 | { 21 | loader: 'ts-loader', 22 | options: { 23 | compilerOptions: { 24 | sourceMap: true 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | ] 31 | }, 32 | output: { 33 | path: path.join(__dirname, 'lib'), 34 | filename: 'index.js', 35 | libraryTarget: 'commonjs' 36 | }, 37 | plugins: [], 38 | node: { 39 | __dirname: false, 40 | __filename: false 41 | } 42 | }; 43 | --------------------------------------------------------------------------------