├── .gitignore ├── .npmignore ├── Readme.md ├── bin ├── observer └── select ├── build.sh ├── lib ├── index.d.ts └── index.js ├── mac ├── observer.m └── select.m ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | tslint.json 4 | *.map 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # coc-imselect 2 | 3 | Input method enhance for vim on mac. 4 | 5 | ![2019-02-26 15_11_49](https://user-images.githubusercontent.com/251450/53394376-0de0c980-39da-11e9-8d6f-8006f98af84f.gif) 6 | 7 | This extension works with vim8 and neovim, latest [coc.nvim](https://github.com/neoclide/coc.nvim) required. 8 | 9 | ## Install 10 | 11 | Install [coc.nvim](https://github.com/neoclide/coc.nvim), then run command: 12 | 13 | ```vim 14 | CocInstall coc-imselect 15 | ``` 16 | 17 | ## Features 18 | 19 | - Monitor input source change and highlight cursor. 20 | - Change input source when necessary on insert. 21 | 22 | ## Options 23 | 24 | - `imselect.defaultInput` default input source use in normal mode, default to `com.apple.keylayout.US`. 25 | - `imselect.enableStatusItem` enable status item in statusline. 26 | - `imselect.enableFloating` enable floating window support for input method. 27 | 28 | ## License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /bin/observer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neoclide/coc-imselect/0251c9e2e44e3c88e4e735a40cdbe9240a5ab6ab/bin/observer -------------------------------------------------------------------------------- /bin/select: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neoclide/coc-imselect/0251c9e2e44e3c88e4e735a40cdbe9240a5ab6ab/bin/select -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p bin 4 | /usr/bin/clang -framework foundation -framework carbon -o bin/observer mac/observer.m 5 | /usr/bin/clang -framework foundation -framework carbon -o bin/select mac/select.m 6 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from 'coc.nvim'; 2 | export declare function activate(context: ExtensionContext): Promise; 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.activate = void 0; 7 | const child_process_1 = require("child_process"); 8 | const coc_nvim_1 = require("coc.nvim"); 9 | const os_1 = __importDefault(require("os")); 10 | const path_1 = __importDefault(require("path")); 11 | const util_1 = require("util"); 12 | const method_cache = new Map(); 13 | let currentMethod; 14 | let currentLang; 15 | async function selectInput(method) { 16 | let cmd = path_1.default.join(__dirname, '../bin/select'); 17 | await util_1.promisify(child_process_1.exec)(`${cmd} ${method}`); 18 | } 19 | async function activate(context) { 20 | let { subscriptions } = context; 21 | let channel = coc_nvim_1.window.createOutputChannel('imselect'); 22 | subscriptions.push(channel); 23 | if (os_1.default.platform() != 'darwin') { 24 | channel.appendLine(`[Error] coc-imselect works on mac only.`); 25 | return; 26 | } 27 | let { nvim } = coc_nvim_1.workspace; 28 | let config = coc_nvim_1.workspace.getConfiguration('imselect'); 29 | let defaultInput = config.get('defaultInput', 'com.apple.keylayout.US'); 30 | let enableFloating = config.get('enableFloating', true); 31 | let floatFactory = new coc_nvim_1.FloatFactory(nvim); 32 | let cmd = path_1.default.join(__dirname, '../bin/observer'); 33 | let task = coc_nvim_1.workspace.createTask('IMSELECT'); 34 | let statusItem; 35 | subscriptions.push(task); 36 | subscriptions.push(floatFactory); 37 | let timer; 38 | task.onStdout(async (input) => { 39 | let curr = input[input.length - 1].trim(); 40 | if (!curr) 41 | return; 42 | let parts = curr.split(/\s/, 2); 43 | if (currentLang == parts[0]) 44 | return; 45 | currentLang = parts[0]; 46 | currentMethod = parts[1]; 47 | if (timer) 48 | clearTimeout(timer); 49 | if (enableFloating) { 50 | floatFactory.show([{ content: currentLang, filetype: '' }]); 51 | timer = setTimeout(() => { 52 | floatFactory.close(); 53 | }, 500); 54 | } 55 | // show float buffer 56 | if (statusItem) { 57 | statusItem.text = currentLang; 58 | } 59 | }); 60 | let exitTimer; 61 | task.onExit(code => { 62 | if (code != 0) { 63 | setTimeout(() => { 64 | coc_nvim_1.window.showErrorMessage(`imselect observer exit with code ${code}`); 65 | }, 500); 66 | } 67 | }); 68 | let running = await task.running; 69 | if (!running) { 70 | task.start({ 71 | cmd, 72 | pty: true 73 | }).then(() => { 74 | channel.appendLine(`[Info] Observer for input change started`); 75 | }, e => { 76 | channel.appendLine(`[Error] Observer error: ${e.message}`); 77 | }); 78 | } 79 | async function selectDefault() { 80 | try { 81 | await selectInput(defaultInput); 82 | } 83 | catch (e) { 84 | coc_nvim_1.window.showErrorMessage(`Error on select input method: ${e.message}`); 85 | } 86 | } 87 | if (config.get('enableStatusItem', true)) { 88 | statusItem = coc_nvim_1.window.createStatusBarItem(0); 89 | statusItem.text = ''; 90 | statusItem.show(); 91 | } 92 | // subscriptions.push(workspace.registerAutocmd({ 93 | // event: 'VimLeavePre', 94 | // request: true, 95 | // callback: selectDefault 96 | // })) 97 | let timeout; 98 | coc_nvim_1.events.on('InsertEnter', async (bufnr) => { 99 | if (timeout) 100 | clearTimeout(timeout); 101 | timeout = setTimeout(() => { 102 | if (coc_nvim_1.events.insertMode && coc_nvim_1.workspace.bufnr == bufnr) { 103 | let method = method_cache.get(bufnr); 104 | if (method && method != currentMethod) { 105 | void selectInput(method); 106 | } 107 | } 108 | }, 50); 109 | }, null, subscriptions); 110 | coc_nvim_1.events.on('InsertLeave', async (bufnr) => { 111 | if (timeout) 112 | clearTimeout(timeout); 113 | method_cache.set(bufnr, currentMethod); 114 | timeout = setTimeout(async () => { 115 | if (!coc_nvim_1.events.insertMode) { 116 | void selectDefault(); 117 | } 118 | }, 50); 119 | }, null, subscriptions); 120 | subscriptions.push(coc_nvim_1.Disposable.create(() => { 121 | if (timer) 122 | clearTimeout(timer); 123 | if (timeout) 124 | clearTimeout(timeout); 125 | if (exitTimer) 126 | clearTimeout(exitTimer); 127 | })); 128 | coc_nvim_1.events.on('FocusGained', async () => { 129 | if (!coc_nvim_1.events.insertMode) 130 | await selectDefault(); 131 | }); 132 | coc_nvim_1.workspace.onDidCloseTextDocument(document => { 133 | let doc = coc_nvim_1.workspace.getDocument(document.uri); 134 | if (doc) 135 | method_cache.delete(doc.bufnr); 136 | }, null, subscriptions); 137 | } 138 | exports.activate = activate; 139 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /mac/observer.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | void currentInput() { 5 | TISInputSourceRef source = TISCopyCurrentKeyboardInputSource(); 6 | NSArray* langs = (NSArray*) TISGetInputSourceProperty(source, kTISPropertyInputSourceLanguages); 7 | NSString* lang = (NSString*) [langs objectAtIndex:0]; 8 | CFStringRef sourceId = (CFStringRef) TISGetInputSourceProperty(source, kTISPropertyInputSourceID); 9 | 10 | fprintf(stdout, "%s %s\n", [lang UTF8String], [(NSString *)sourceId UTF8String]); 11 | } 12 | 13 | void notificationCallback (CFNotificationCenterRef center, void * observer, CFStringRef name, const void * object, CFDictionaryRef userInfo) { 14 | currentInput(); 15 | } 16 | 17 | int main(int argc, const char * argv[]) { 18 | currentInput(); 19 | CFNotificationCenterRef center = CFNotificationCenterGetDistributedCenter(); 20 | CFNotificationCenterAddObserver(center, NULL, notificationCallback, 21 | kTISNotifySelectedKeyboardInputSourceChanged, NULL, 22 | CFNotificationSuspensionBehaviorDeliverImmediately); 23 | CFRunLoopRun(); 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /mac/select.m: -------------------------------------------------------------------------------- 1 | // 2 | // im-select 3 | // 4 | // Created by Ying Bian on 8/21/12. 5 | // Copyright (c) 2012 Ying Bian. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | int ret = 0; 13 | @autoreleasepool { 14 | if (argc > 1) { 15 | NSString *inputSource = [NSString stringWithUTF8String:argv[1]]; 16 | NSDictionary *filter = [NSDictionary dictionaryWithObject:inputSource forKey:(NSString *)kTISPropertyInputSourceID]; 17 | CFArrayRef keyboards = TISCreateInputSourceList((__bridge CFDictionaryRef)filter, false); 18 | if (keyboards) { 19 | TISInputSourceRef selected = (TISInputSourceRef)CFArrayGetValueAtIndex(keyboards, 0); 20 | ret = TISSelectInputSource(selected); 21 | CFRelease(keyboards); 22 | } else { 23 | ret = 1; 24 | } 25 | } else { 26 | TISInputSourceRef currentInputSource = TISCopyCurrentKeyboardInputSource(); 27 | NSString *sourceId = (__bridge NSString *)(TISGetInputSourceProperty(currentInputSource, kTISPropertyInputSourceID)); 28 | printf("%s\n", [sourceId UTF8String]); 29 | CFRelease(currentInputSource); 30 | } 31 | } 32 | return ret; 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-imselect", 3 | "version": "0.0.15", 4 | "description": "Input method extension for coc.nvim on mac.", 5 | "main": "lib/index.js", 6 | "publisher": "chemzqm", 7 | "keywords": [ 8 | "coc.nvim", 9 | "input" 10 | ], 11 | "engines": { 12 | "coc": "^0.0.80" 13 | }, 14 | "scripts": { 15 | "build": "tsc -p tsconfig.json", 16 | "postinstall": "./build.sh", 17 | "prepare": "tsc -p tsconfig.json" 18 | }, 19 | "activationEvents": [ 20 | "*" 21 | ], 22 | "contributes": { 23 | "configuration": { 24 | "type": "object", 25 | "properties": { 26 | "imselect.defaultInput": { 27 | "type": "string", 28 | "default": "com.apple.keylayout.US", 29 | "description": "default input source use in normal mode" 30 | }, 31 | "imselect.enableFloating": { 32 | "type": "boolean", 33 | "default": true, 34 | "description": "Enable floating for input method when possible" 35 | }, 36 | "imselect.enableStatusItem": { 37 | "type": "boolean", 38 | "default": true 39 | } 40 | } 41 | } 42 | }, 43 | "author": "chemzqm@gmail.com", 44 | "license": "MIT", 45 | "devDependencies": { 46 | "@chemzqm/tsconfig": "^0.0.3", 47 | "@types/node": "^10.12.24", 48 | "coc.nvim": "0.0.81-next.29", 49 | "typescript": "^4.1.3" 50 | }, 51 | "dependencies": {} 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { Disposable, events, ExtensionContext, FloatFactory, StatusBarItem, window, workspace } from 'coc.nvim' 3 | import os from 'os' 4 | import path from 'path' 5 | import { promisify } from 'util' 6 | 7 | const method_cache: Map = new Map() 8 | let currentMethod: string 9 | let currentLang: string 10 | 11 | async function selectInput(method: string): Promise { 12 | let cmd = path.join(__dirname, '../bin/select') 13 | await promisify(exec)(`${cmd} ${method}`) 14 | } 15 | 16 | export async function activate(context: ExtensionContext): Promise { 17 | let { subscriptions } = context 18 | let channel = window.createOutputChannel('imselect') 19 | subscriptions.push(channel) 20 | if (os.platform() != 'darwin') { 21 | channel.appendLine(`[Error] coc-imselect works on mac only.`) 22 | return 23 | } 24 | let { nvim } = workspace 25 | let config = workspace.getConfiguration('imselect') 26 | let defaultInput = config.get('defaultInput', 'com.apple.keylayout.US') 27 | let enableFloating = config.get('enableFloating', true) 28 | let floatFactory = new FloatFactory(nvim) 29 | let cmd = path.join(__dirname, '../bin/observer') 30 | let task = workspace.createTask('IMSELECT') 31 | let statusItem: StatusBarItem 32 | subscriptions.push(task) 33 | subscriptions.push(floatFactory) 34 | 35 | let timer: NodeJS.Timer 36 | task.onStdout(async input => { 37 | let curr = input[input.length - 1].trim() 38 | if (!curr) return 39 | let parts = curr.split(/\s/, 2) 40 | if (currentLang == parts[0]) return 41 | currentLang = parts[0] 42 | currentMethod = parts[1] 43 | if (timer) clearTimeout(timer) 44 | if (enableFloating) { 45 | floatFactory.show([{ content: currentLang, filetype: '' }]) 46 | timer = setTimeout(() => { 47 | floatFactory.close() 48 | }, 500) 49 | } 50 | // show float buffer 51 | if (statusItem) { 52 | statusItem.text = currentLang 53 | } 54 | }) 55 | let exitTimer: NodeJS.Timeout 56 | task.onExit(code => { 57 | if (code != 0) { 58 | setTimeout(() => { 59 | window.showErrorMessage(`imselect observer exit with code ${code}`) 60 | }, 500) 61 | } 62 | }) 63 | let running = await task.running 64 | if (!running) { 65 | task.start({ 66 | cmd, 67 | pty: true 68 | }).then(() => { 69 | channel.appendLine(`[Info] Observer for input change started`) 70 | }, e => { 71 | channel.appendLine(`[Error] Observer error: ${e.message}`) 72 | }) 73 | } 74 | 75 | async function selectDefault(): Promise { 76 | try { 77 | await selectInput(defaultInput) 78 | } catch (e) { 79 | window.showErrorMessage(`Error on select input method: ${e.message}`) 80 | } 81 | } 82 | 83 | if (config.get('enableStatusItem', true)) { 84 | statusItem = window.createStatusBarItem(0) 85 | statusItem.text = '' 86 | statusItem.show() 87 | } 88 | 89 | // subscriptions.push(workspace.registerAutocmd({ 90 | // event: 'VimLeavePre', 91 | // request: true, 92 | // callback: selectDefault 93 | // })) 94 | 95 | let timeout: NodeJS.Timeout 96 | events.on('InsertEnter', async (bufnr) => { 97 | if (timeout) clearTimeout(timeout) 98 | timeout = setTimeout(() => { 99 | if (events.insertMode && workspace.bufnr == bufnr) { 100 | let method = method_cache.get(bufnr) 101 | if (method && method != currentMethod) { 102 | void selectInput(method) 103 | } 104 | } 105 | }, 50) 106 | }, null, subscriptions) 107 | 108 | events.on('InsertLeave', async bufnr => { 109 | if (timeout) clearTimeout(timeout) 110 | method_cache.set(bufnr, currentMethod) 111 | timeout = setTimeout(async () => { 112 | if (!events.insertMode) { 113 | void selectDefault() 114 | } 115 | }, 50) 116 | }, null, subscriptions) 117 | 118 | subscriptions.push(Disposable.create(() => { 119 | if (timer) clearTimeout(timer) 120 | if (timeout) clearTimeout(timeout) 121 | if (exitTimer) clearTimeout(exitTimer) 122 | })) 123 | 124 | events.on('FocusGained', async () => { 125 | if (!events.insertMode) await selectDefault() 126 | }) 127 | 128 | workspace.onDidCloseTextDocument(document => { 129 | let doc = workspace.getDocument(document.uri) 130 | if (doc) method_cache.delete(doc.bufnr) 131 | }, null, subscriptions) 132 | } 133 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@chemzqm/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "target": "es2017", 6 | "module": "commonjs", 7 | "noUnusedLocals": true, 8 | "moduleResolution": "node", 9 | "lib": ["es2018"], 10 | "plugins": [] 11 | }, 12 | "include": ["src"], 13 | "exclude": [] 14 | } 15 | -------------------------------------------------------------------------------- /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.yarnpkg.com/@chemzqm/tsconfig/-/tsconfig-0.0.3.tgz#ce3480d15d8cec46a315488caa07c9fca819aecc" 8 | integrity sha512-MjF25vbqLYR+S+JJLgBi0vn4gZqv/C87H+yPSlVKEqlIJAJOGJOgFPUFvRS7pdRHqkv2flX/oRxzxhlu2V0X1w== 9 | 10 | "@types/node@^10.12.24": 11 | version "10.17.49" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.49.tgz#ecf0b67bab4b84d0ec9b0709db4aac3824a51c4a" 13 | integrity sha512-PGaJNs5IZz5XgzwJvL/1zRfZB7iaJ5BydZ8/Picm+lUNYoNO9iVTQkVy5eUh0dZDrx3rBOIs3GCbCRmMuYyqwg== 14 | 15 | coc.nvim@0.0.81-next.29: 16 | version "0.0.81-next.29" 17 | resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.29.tgz#7d47d1e50713ba4886222ea0362f445d00b3d4ea" 18 | integrity sha512-SeK0QVvIavVt20UtnMoL0d4bY+m5OgMeo6ws2IyA/jggMYHWPhgSVr9a5CRefH5jCDstsrgsqY85aheujlGt+A== 19 | 20 | typescript@^4.1.3: 21 | version "4.1.3" 22 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" 23 | integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== 24 | --------------------------------------------------------------------------------