├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _images └── completion.gif ├── autoload └── autocomplete_swift.vim ├── plugin └── autocomplete_swift.vim └── rplugin └── python3 └── deoplete └── sources └── swift.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.12.0 2 | 3 | - Present the return type of function, method and variable on the completion window. 4 | 5 | 6 | ## v0.11.1 7 | 8 | - Recover from degrading on handling placeholders. 9 | 10 | 11 | ## v0.11.0 12 | 13 | - Add support for completion on SPM-based project. 14 | 15 | 16 | ## v0.10.0 17 | 18 | - Improve presentation of candidates: 19 | - Use `descriptionKey` for the abbreviation of candidate to present the signature of function. 20 | - Present the kind of candidate. 21 | 22 | 23 | ## v0.9.0 24 | 25 | - Add support for switching the version of Swift. 26 | - Drop experimental support for Xcode project because of the issue on completion server. 27 | - Drop support for Vim. 28 | 29 | 30 | ## v0.8.0 31 | 32 | - Support completion for whole parameters of initializer, method and function with `(`. 33 | 34 | 35 | ## v0.7.1 36 | 37 | - Fix: Filter new-line characters in complesion candidate, especially in method definition. 38 | - Fix: Avoid conversion of placeholders to neosnippet-style when neosnippet is disabled. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tomoya Kose. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autocomplete-swift 2 | 3 | [![License][license-badge]][license] 4 | [![Release][release-badge]][release] 5 | 6 | Autocompletion for Swift in [NeoVim][web-neovim] with [deoplete][github-deoplete]. 7 | 8 | ![completion-gif](/_images/completion.gif) 9 | 10 | 11 | ## Announcement 12 | 13 | - The support for **completion on SPM-based project** is added. 14 | - Autocompletion-swift **droped support for Vim** and completion with omni-function. 15 | Please **use this plugin in NeoVim** with deoplete.nvim. 16 | 17 | 18 | ## Installation 19 | 20 | Autocomplete-swift uses [SourceKitten][github-sourcekitten] as its back-end. 21 | SourceKitten can be installed with [Homebrew][github-homebrew]. 22 | This plugin also requires [PyYaml][web-pyyaml]. 23 | 24 | Please execute the following commands: 25 | 26 | ```bash 27 | $ brew install sourcekitten 28 | $ pip install pyyaml 29 | ``` 30 | 31 | To install autocomplete-swift, 32 | it is recommended to use plugin manager such as [dein.vim][github-dein]. 33 | In the case of dein.vim, please add the following codes into `init.vim` and configure them: 34 | 35 | ```vim 36 | call dein#add('Shougo/deoplete.nvim') 37 | call dein#add('mitsuse/autocomplete-swift') 38 | ``` 39 | 40 | This plugin also supports jumping to placeholders in arguments of method. 41 | The following configuration is required: 42 | 43 | ```vim 44 | " Jump to the first placeholder by typing ``. 45 | autocmd FileType swift imap (autocomplete_swift_jump_to_placeholder) 46 | ``` 47 | 48 | If you use [neosnippet][github-neosnippet], 49 | you should enable [key-mappings of neosnippets][github-neosnippet-config] instead of using the above code. 50 | Autocomplete-swift gets along with neosnippet by converting placeholders into its ones. 51 | 52 | ### `swift` filetype 53 | 54 | If your vim setup doesn't set `*.swift` file's `filetype` to `swift` 55 | you need to put this line in your config: 56 | 57 | ``` 58 | autocmd BufNewFile,BufRead *.swift set filetype=swift 59 | ``` 60 | 61 | 62 | ## Features 63 | 64 | ### Completion 65 | 66 | Autocomplete-swift supports types of completion as follow: 67 | 68 | - Type name 69 | - Type/Instance member 70 | - Function/method/initializer parameter 71 | - Top-level function/constant/variable 72 | - Keyword such as `protocol`, `extension` etc. 73 | - Method definition 74 | 75 | 76 | ### Placeholder 77 | 78 | This plugin supports jumping to placeholders in arguments of method. 79 | Please read [Installation](#installation). 80 | 81 | 82 | ### Custom Toolchain 83 | 84 | The custom toolchain is available for completion. 85 | For example, if you want to use Swift 4.2, 86 | call `autocomplete_swift#use_toolchain('Swift_4_2')` or 87 | `autocomplete_swift#use_custom_toolchain('com.apple.dt.toolchain.Swift_4_2')`. 88 | 89 | 90 | ### Support for Swift Package Manager (SPM) 91 | 92 | When you are editing a file managed with SPM, autocomplete-swift enables SPM-based completion. 93 | It means that you can obtain candidates which come from dependencies (other files or libraries). 94 | 95 | 96 | ### Xcode Project Support 97 | 98 | The previous version supported completion with framework/SDK experimentally, 99 | but the feature is removed because the completion server has fatal bugs. 100 | 101 | 102 | ## TODO 103 | 104 | - Make configurable. For example, autocomplete-swift will get `max_candiates` for deoplete from a variable. 105 | 106 | 107 | ## Related project 108 | 109 | In the GIF on the beginning, 110 | I use snippets for Swift contained in [neosnippet-snippets][github-neosnippet-snippets] 111 | in addition to autocomplete-swift. 112 | 113 | 114 | ## License 115 | 116 | Please read [LICENSE][license]. 117 | 118 | [license-badge]: https://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat-square 119 | [license]: LICENSE 120 | [release-badge]: https://img.shields.io/github/tag/mitsuse/autocomplete-swift.svg?style=flat-square 121 | [release]: https://github.com/mitsuse/autocomplete-swift/releases 122 | [github-sourcekitten]: https://github.com/jpsim/SourceKitten 123 | [github-sourcekittendaemon]: https://github.com/terhechte/SourceKittenDaemon 124 | [github-homebrew]: https://github.com/Homebrew/homebrew-core 125 | [github-neosnippet]: https://github.com/Shougo/neosnippet.vim 126 | [github-neosnippet-config]: https://github.com/Shougo/neosnippet.vim#configuration 127 | [github-neosnippet-snippets]: https://github.com/Shougo/neosnippet-snippets 128 | [github-deoplete]: https://github.com/Shougo/deoplete.nvim 129 | [github-dein]: https://github.com/Shougo/dein.vim 130 | [github-yata]: https://github.com/mitsuse/yata 131 | [web-neovim]: https://neovim.io/ 132 | [web-pyyaml]: http://pyyaml.org/ 133 | -------------------------------------------------------------------------------- /_images/completion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuse/autocomplete-swift/280f3fbb6bd5b8b33ec58f3e4597ba8eaa5e8784/_images/completion.gif -------------------------------------------------------------------------------- /autoload/autocomplete_swift.vim: -------------------------------------------------------------------------------- 1 | " Use the toolchain specified with `com.apple.dt.toolchain.{name}`. 2 | function! autocomplete_swift#use_toolchain(name) 3 | call autocomplete_swift#use_custom_toolchain(s:get_xcode_toolchain(a:name)) 4 | endfunction 5 | 6 | " Use the custom toolchain specified with the given identifier. 7 | function! autocomplete_swift#use_custom_toolchain(identifier) 8 | let g:autocomplete_swift#toolchain = a:identifier 9 | endfunction 10 | 11 | func s:get_xcode_toolchain(name) 12 | return 'com.apple.dt.toolchain.' . a:name 13 | endfunction 14 | 15 | function! autocomplete_swift#jump_to_placeholder() 16 | if &filetype !=# 'swift' 17 | return '' 18 | end 19 | 20 | if !autocomplete_swift#check_placeholder_existence() 21 | return '' 22 | endif 23 | 24 | return "\:call autocomplete_swift#begin_replacing_placeholder()\" 25 | endfunction 26 | 27 | function! autocomplete_swift#check_placeholder_existence() 28 | return search(autocomplete_swift#generate_placeholder_pattern()) 29 | endfunction 30 | 31 | function! autocomplete_swift#begin_replacing_placeholder() 32 | if mode() !=# 'n' 33 | return 34 | endif 35 | 36 | let l:pattern = autocomplete_swift#generate_placeholder_pattern() 37 | 38 | let [l:line, l:column] = searchpos(l:pattern) 39 | if l:line == 0 && l:column == 0 40 | return 41 | end 42 | 43 | execute printf(':%d s/%s//', l:line, l:pattern) 44 | 45 | call cursor(l:line, l:column) 46 | 47 | startinsert 48 | endfunction 49 | 50 | function! autocomplete_swift#generate_placeholder_pattern() 51 | return '<#\%(T##\)\?\%([^#]\+##\)\?\([^#]\+\)#>' 52 | endfunction 53 | -------------------------------------------------------------------------------- /plugin/autocomplete_swift.vim: -------------------------------------------------------------------------------- 1 | inoremap (autocomplete_swift_jump_to_placeholder) autocomplete_swift#jump_to_placeholder() 2 | -------------------------------------------------------------------------------- /rplugin/python3/deoplete/sources/swift.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from .base import Base 5 | 6 | _KIND_PREFIX_FUNCTION = 'source.lang.swift.decl.function' 7 | _KIND_PREFIX_VAR = 'source.lang.swift.decl.var' 8 | 9 | 10 | class Source(Base): 11 | def __init__(self, vim): 12 | Base.__init__(self, vim) 13 | 14 | self.name = 'swift' 15 | self.mark = '[swift]' 16 | self.filetypes = ['swift'] 17 | self.min_pattern_length = 4 18 | self.max_pattern_length = 30 19 | self.input_pattern = '((?:\.|(?:,|:|->)\s+)\w*|\()' 20 | 21 | self.__completer = Completer(vim) 22 | 23 | def get_complete_position(self, context): 24 | return self.__completer.decide_completion_position( 25 | context['input'], 26 | int(self.vim.eval('col(\'.\')')) 27 | ) 28 | 29 | def gather_candidates(self, context): 30 | return self.__completer.complete( 31 | int(self.vim.eval('line(\'.\')')), 32 | context['complete_position'] + 1 33 | ) 34 | 35 | 36 | class Completer(object): 37 | def __init__(self, vim): 38 | import re 39 | 40 | self.__vim = vim 41 | self.__completion_pattern = re.compile('\w*$') 42 | self.__placeholder_pattern = re.compile( 43 | '<#(?:T##)?(?:[^#]+##)?(?P[^#]+)#>' 44 | ) 45 | 46 | def complete(self, line, column): 47 | path, content, offset = self.__prepare_completion(line, column) 48 | 49 | completer = self.__decide_completer() 50 | candidates_json = completer.complete(path, content, offset) 51 | 52 | return [self.__convert_candidates(c) for c in candidates_json] 53 | 54 | def decide_completion_position(self, text, column): 55 | result = self.__completion_pattern.search(text) 56 | 57 | if result is None: 58 | return column - 1 59 | 60 | return result.start() 61 | 62 | def __decide_completer(self): 63 | try: 64 | toolchain = self.__vim.eval('autocomplete_swift#toolchain') 65 | except: 66 | toolchain = None 67 | 68 | try: 69 | command = self.__vim.eval('autocomplete_swift#sourcekitten_command') 70 | except: 71 | command = 'sourcekitten' 72 | 73 | return SourceKitten(command=command, toolchain=toolchain) 74 | 75 | def __prepare_completion(self, row, column): 76 | text_list = self.__vim.current.buffer[:] 77 | encoding = self.__vim.options['encoding'] 78 | path = self.__vim.call('expand', '%:p') 79 | if len(path) == 0: 80 | path = None 81 | 82 | content = '\n'.join(text_list) 83 | 84 | offset = 0 85 | for row_current, text in enumerate(text_list): 86 | if row_current < row - 1: 87 | offset += len(bytes(text, encoding)) + 1 88 | offset += column - 1 89 | 90 | return (path, content, offset) 91 | 92 | def __filter_newline(self, text): 93 | return text.replace('\n', '') 94 | 95 | def __extract_abbreviation(self, json): 96 | kind = json['kind'] 97 | if kind.startswith(_KIND_PREFIX_FUNCTION): 98 | return '{} -> {}'.format(json['descriptionKey'], json['typeName']) 99 | elif kind.startswith(_KIND_PREFIX_VAR): 100 | return '{}: {}'.format(json['descriptionKey'], json['typeName']) 101 | else: 102 | return json['descriptionKey'] 103 | 104 | def __convert_candidates(self, json): 105 | return { 106 | 'word': self.__filter_newline(self.__convert_placeholder(json['sourcetext'])), 107 | 'abbr': self.__extract_abbreviation(json), 108 | 'kind': json['kind'].split('.')[-1] 109 | } 110 | 111 | def __convert_placeholder(self, text): 112 | variables = {'index': 0} 113 | 114 | neosnippet_func = '*neosnippet#get_snippets_directory' 115 | used_neosnippet = int(self.__vim.call('exists', neosnippet_func)) == 1 116 | 117 | if not used_neosnippet: 118 | return text 119 | 120 | def replacer(match): 121 | try: 122 | description = match.group('desc') 123 | variables['index'] += 1 124 | return '<`{}:{}`>'.format(variables['index'], description) 125 | 126 | except IndexError: 127 | return '' 128 | 129 | return self.__placeholder_pattern.sub(replacer, text) 130 | 131 | 132 | class SourceKitten(object): 133 | def __init__(self, command='sourcekitten', toolchain=None): 134 | import os 135 | 136 | self.__command = command 137 | self.__environment = os.environ.copy() 138 | 139 | if toolchain is not None: 140 | self.__environment['TOOLCHAINS'] = toolchain 141 | 142 | def complete(self, path, content, offset): 143 | import os 144 | import subprocess 145 | import tempfile 146 | import json 147 | 148 | if not self.is_executable: 149 | return [] 150 | 151 | temporary_path = tempfile.mktemp() 152 | try: 153 | with open(temporary_path, 'w') as f: 154 | f.write(content) 155 | except: 156 | return [] 157 | 158 | arguments = SourceKitten.generate_arguments(path, temporary_path) 159 | 160 | try: 161 | command = [ 162 | self.__command, 163 | 'complete', 164 | '--file', temporary_path, 165 | '--offset', str(offset) 166 | ] + arguments 167 | 168 | output, _ = subprocess.Popen( 169 | command, 170 | stdout=subprocess.PIPE, 171 | env=self.__environment 172 | ).communicate() 173 | 174 | os.remove(temporary_path) 175 | 176 | return json.loads(output.decode()) 177 | 178 | except subprocess.CalledProcessError: 179 | os.remove(temporary_path) 180 | 181 | return [] 182 | 183 | @property 184 | def is_executable(self): 185 | import shutil 186 | 187 | return shutil.which(self.__command) is not None 188 | 189 | @staticmethod 190 | def generate_arguments(path, temporary_path): 191 | import os 192 | 193 | if path is None: 194 | return [] 195 | absolute_path = os.path.abspath(path) 196 | 197 | project = Project.find_from_source(absolute_path) 198 | if project is None: 199 | return [] 200 | 201 | module = project.find_module(absolute_path) 202 | if module is None: 203 | return [] 204 | 205 | arguments = \ 206 | ['--', temporary_path] + \ 207 | list(filter(lambda x: x != absolute_path, module.sources)) + \ 208 | ['-module-name', module.name] + \ 209 | module.other_arguments + \ 210 | ['-I'] + \ 211 | list(module.imports) 212 | 213 | return arguments 214 | 215 | 216 | class Project(object): 217 | def __init__(self, path, modules): 218 | import os 219 | 220 | self.path = path 221 | self.modules = modules 222 | 223 | @staticmethod 224 | def load(path): 225 | import os 226 | 227 | with open(os.path.join(path, '.build', 'debug.yaml')) as f: 228 | modules = Module.load(f) 229 | 230 | return Project(path, modules) 231 | 232 | def find_module(self, source_path): 233 | import os 234 | 235 | absolute_path = os.path.abspath(source_path) 236 | 237 | for module in self.modules: 238 | if absolute_path in module.sources: 239 | return module 240 | 241 | return None 242 | 243 | @staticmethod 244 | def find_from_source(path): 245 | root = Project.find_root_from_source(path) 246 | if root is None: 247 | return None 248 | 249 | try: 250 | return Project.load(root) 251 | except: 252 | return None 253 | 254 | @staticmethod 255 | def find_root_from_source(path): 256 | import os 257 | 258 | previous_path = path 259 | while True: 260 | current_path = os.path.dirname(os.path.abspath(previous_path)) 261 | if current_path == previous_path: 262 | break 263 | 264 | description_path = os.path.join(current_path, 'Package.swift') 265 | if os.path.isfile(description_path): 266 | return current_path 267 | 268 | previous_path = current_path 269 | 270 | return None 271 | 272 | 273 | class Module(object): 274 | def __init__(self, name, sources, imports, other_arguments): 275 | self.name = name 276 | self.sources = sources 277 | self.imports = imports 278 | self.other_arguments = other_arguments 279 | 280 | @staticmethod 281 | def load(f): 282 | import yaml 283 | 284 | obj = yaml.load(f) 285 | 286 | def convert_to_log(obj): 287 | name = obj.get('module-name') 288 | if name is None: 289 | return None 290 | 291 | return Module( 292 | name, 293 | set(obj.get('sources', [])), 294 | set(obj.get('import-paths', [])), 295 | obj.get('other-args', []) 296 | ) 297 | 298 | generator = filter( 299 | lambda x: x is not None, 300 | map( 301 | lambda x: convert_to_log(x), 302 | obj.get('commands', {}).values() 303 | ) 304 | ) 305 | 306 | return list(generator) 307 | --------------------------------------------------------------------------------