diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..e39575fb --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,50 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project maintainer at val@markovic.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ diff --git a/README.md b/README.md index fbaa46f7..073b98ab 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ YouCompleteMe: a code-completion engine for Vim - [YcmCompleter subcommands](#ycmcompleter-subcommands) - [Options](#options) - [FAQ](#faq) +- [Contributor Code of Conduct](#contributor-code-of-conduct) - [Contact](#contact) - [License](#license) @@ -2609,6 +2610,14 @@ os.environ['PATH'] = ';'.join(path) EOF ``` +Contributor Code of Conduct +--------------------------- + +Please note that this project is released with a [Contributor Code of +Conduct][ccoc]. By participating in this project you agree to abide by its +terms. + + Contact ------- @@ -2623,6 +2632,7 @@ The latest version of the plugin is available at The author's homepage is . + License ------- @@ -2684,4 +2694,4 @@ This software is licensed under the [GPL v3 license][gpl]. [rust-src]: https://www.rust-lang.org/downloads.html [add-msbuild-to-path]: http://stackoverflow.com/questions/6319274/how-do-i-run-msbuild-from-the-command-line-using-windows-sdk-7-1 [identify-R6034-cause]: http://stackoverflow.com/questions/14552348/runtime-error-r6034-in-embedded-python-application/34696022 - +[ccoc]: https://github.com/Valloric/YouCompleteMe/blob/master/CODE_OF_CONDUCT.md diff --git a/appveyor.yml b/appveyor.yml index 88817a87..3a5aba4a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,9 +6,9 @@ environment: install: - git submodule update --init --recursive - ps: $env:python = if ($env:arch -eq 32) { 'C:\Python27' } else { 'C:\Python27-x64' } - - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:\get-pip.py') + - appveyor DownloadFile https://bootstrap.pypa.io/get-pip.py - set PATH=%python%;%python%\Scripts;%PATH% - - python C:\get-pip.py + - python get-pip.py - pip install -r python\test_requirements.txt build_script: - python run_tests.py diff --git a/python/ycm/client/omni_completion_request.py b/python/ycm/client/omni_completion_request.py index ddf9d0b6..209eead3 100644 --- a/python/ycm/client/omni_completion_request.py +++ b/python/ycm/client/omni_completion_request.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +from ycmd.utils import ToUtf8IfNeeded from ycm.client.completion_request import CompletionRequest @@ -34,5 +35,32 @@ class OmniCompletionRequest( CompletionRequest ): return True + def RawResponse( self ): + return _ConvertVimDatasToCompletionDatas( self._results ) + + def Response( self ): return self._results + + +def ConvertVimDataToCompletionData( vim_data ): + # see :h complete-items for a description of the dictionary fields + completion_data = {} + + if 'word' in vim_data: + completion_data[ 'insertion_text' ] = ToUtf8IfNeeded( vim_data[ 'word' ] ) + if 'abbr' in vim_data: + completion_data[ 'menu_text' ] = ToUtf8IfNeeded( vim_data[ 'abbr' ] ) + if 'menu' in vim_data: + completion_data[ 'extra_menu_info' ] = ToUtf8IfNeeded( vim_data[ 'menu' ] ) + if 'kind' in vim_data: + completion_data[ 'kind' ] = [ ToUtf8IfNeeded( vim_data[ 'kind' ] ) ] + if 'info' in vim_data: + completion_data[ 'detailed_info' ] = ToUtf8IfNeeded( vim_data[ 'info' ] ) + + return completion_data + + +def _ConvertVimDatasToCompletionDatas( response_data ): + return [ ConvertVimDataToCompletionData( x ) + for x in response_data ] diff --git a/python/ycm/paths.py b/python/ycm/paths.py index b5e1373d..e103778c 100644 --- a/python/ycm/paths.py +++ b/python/ycm/paths.py @@ -18,14 +18,16 @@ # along with YouCompleteMe. If not, see . import os +import sys import vim import functools +import re from ycmd import utils DIR_OF_CURRENT_SCRIPT = os.path.dirname( os.path.abspath( __file__ ) ) -WIN_PYTHON27_PATH = 'C:\python27\python.exe' -WIN_PYTHON26_PATH = 'C:\python26\python.exe' +WIN_PYTHON_PATH = os.path.join( sys.exec_prefix, 'python.exe' ) +PYTHON_BINARY_REGEX = re.compile( r'python(2(\.[67])?)?(.exe)?$' ) def Memoize( obj ): @@ -42,29 +44,56 @@ def Memoize( obj ): @Memoize def PathToPythonInterpreter(): - user_path_to_python = vim.eval( 'g:ycm_path_to_python_interpreter' ) + python_interpreter = vim.eval( 'g:ycm_path_to_python_interpreter' ) - if user_path_to_python: - return user_path_to_python + if python_interpreter: + if IsPythonVersionCorrect( python_interpreter ): + return python_interpreter - # We check for 'python2' before 'python' because some OS's (I'm looking at - # you Arch Linux) have made the... interesting decision to point - # /usr/bin/python to python3. - python_names = [ 'python2', 'python' ] + raise RuntimeError( "Path in 'g:ycm_path_to_python_interpreter' option " + "does not point to a valid Python 2.6 or 2.7." ) - path_to_python = utils.PathToFirstExistingExecutable( python_names ) - if path_to_python: - return path_to_python + # On UNIX platforms, we use sys.executable as the Python interpreter path. + # We cannot use sys.executable on Windows because for unknown reasons, it + # returns the Vim executable. Instead, we use sys.exec_prefix to deduce the + # interpreter path. + python_interpreter = ( WIN_PYTHON_PATH if utils.OnWindows() else + sys.executable ) - # On Windows, Python may not be on the PATH at all, so we check some common - # install locations. - if utils.OnWindows(): - if os.path.exists( WIN_PYTHON27_PATH ): - return WIN_PYTHON27_PATH - elif os.path.exists( WIN_PYTHON26_PATH ): - return WIN_PYTHON26_PATH + if IsPythonVersionCorrect( python_interpreter ): + return python_interpreter - raise RuntimeError( 'Python 2.7/2.6 not installed!' ) + # As a last resort, we search python in the PATH. We check 'python2' before + # 'python' because on some distributions (Arch Linux for example), python + # refers to python3. + python_interpreter = utils.PathToFirstExistingExecutable( [ 'python2', + 'python' ] ) + + if IsPythonVersionCorrect( python_interpreter ): + return python_interpreter + + raise RuntimeError( "Cannot find Python 2.6 or 2.7. You can set its path " + "using the 'g:ycm_path_to_python_interpreter' " + "option." ) + + +def EndsWithPython( path ): + """Check if given path ends with a python 2.6 or 2.7 name.""" + return PYTHON_BINARY_REGEX.search( path ) is not None + + +def IsPythonVersionCorrect( path ): + """Check if given path is the Python interpreter version 2.6 or 2.7.""" + if not EndsWithPython( path ): + return False + + command = [ path, + '-c', + "import sys;" + "major, minor = sys.version_info[ :2 ];" + "sys.exit( major != 2 or minor < 6)" ] + + return utils.SafePopen( command ).wait() == 0 def PathToServerScript(): diff --git a/python/ycm/test_utils.py b/python/ycm/test_utils.py index 4f9da546..20f1b88a 100644 --- a/python/ycm/test_utils.py +++ b/python/ycm/test_utils.py @@ -18,6 +18,7 @@ # along with YouCompleteMe. If not, see . from mock import MagicMock +from hamcrest import assert_that, equal_to import re import sys @@ -116,3 +117,10 @@ def MockVimModule(): sys.modules[ 'vim' ] = VIM_MOCK return VIM_MOCK + + +class ExtendedMock( MagicMock ): + + def assert_has_exact_calls( self, calls, any_order = False ): + self.assert_has_calls( calls, any_order ) + assert_that( self.call_count, equal_to( len( calls ) ) ) diff --git a/python/ycm/tests/event_notification_test.py b/python/ycm/tests/event_notification_test.py index f7ee868f..68585f63 100644 --- a/python/ycm/tests/event_notification_test.py +++ b/python/ycm/tests/event_notification_test.py @@ -16,10 +16,11 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . -from ycm.test_utils import MockVimModule +from ycm.test_utils import MockVimModule, ExtendedMock MockVimModule() -import contextlib, os +import contextlib +import os from ycm.youcompleteme import YouCompleteMe from ycmd import user_options_store @@ -36,6 +37,7 @@ DEFAULT_CLIENT_OPTIONS = { 'extra_conf_vim_data': [], } + def PostVimMessage_Call( message ): """Return a mock.call object for a call to vimsupport.PostVimMesasge with the supplied message""" @@ -74,7 +76,7 @@ def MockArbitraryBuffer( filetype, native_available = True ): raise ValueError( 'Unexpected evaluation' ) # Arbitrary, but valid, cursor position - vim_current.window.cursor = (1,2) + vim_current.window.cursor = ( 1, 2 ) # Arbitrary, but valid, single buffer open current_buffer = MagicMock() @@ -129,7 +131,7 @@ class EventNotification_test( object ): def setUp( self ): options = dict( user_options_store.DefaultOptions() ) - options.update ( DEFAULT_CLIENT_OPTIONS ) + options.update( DEFAULT_CLIENT_OPTIONS ) user_options_store.SetAll( options ) self.server_state = YouCompleteMe( user_options_store.GetAll() ) @@ -141,7 +143,7 @@ class EventNotification_test( object ): self.server_state.OnVimLeave() - @patch( 'vim.command' ) + @patch( 'vim.command', new_callable = ExtendedMock ) def FileReadyToParse_NonDiagnostic_Error_test( self, vim_command ): # This test validates the behaviour of YouCompleteMe.ValidateParseRequest in # combination with YouCompleteMe.OnFileReadyToParse when the completer @@ -158,13 +160,13 @@ class EventNotification_test( object ): self.server_state.ValidateParseRequest() # The first call raises a warning - vim_command.assert_has_calls( [ + vim_command.assert_has_exact_calls( [ PostVimMessage_Call( ERROR_TEXT ), ] ) # Subsequent calls don't re-raise the warning self.server_state.ValidateParseRequest() - vim_command.assert_has_calls( [ + vim_command.assert_has_exact_calls( [ PostVimMessage_Call( ERROR_TEXT ), ] ) @@ -172,7 +174,7 @@ class EventNotification_test( object ): self.server_state.OnFileReadyToParse() assert self.server_state.DiagnosticsForCurrentFileReady() self.server_state.ValidateParseRequest() - vim_command.assert_has_calls( [ + vim_command.assert_has_exact_calls( [ PostVimMessage_Call( ERROR_TEXT ), PostVimMessage_Call( ERROR_TEXT ), ] ) @@ -187,8 +189,10 @@ class EventNotification_test( object ): vim_command.assert_not_called() - @patch( 'ycm.client.event_notification._LoadExtraConfFile' ) - @patch( 'ycm.client.event_notification._IgnoreExtraConfFile' ) + @patch( 'ycm.client.event_notification._LoadExtraConfFile', + new_callable = ExtendedMock ) + @patch( 'ycm.client.event_notification._IgnoreExtraConfFile', + new_callable = ExtendedMock ) def FileReadyToParse_NonDiagnostic_ConfirmExtraConf_test( self, ignore_extra_conf, @@ -211,25 +215,26 @@ class EventNotification_test( object ): # When the user accepts the extra conf, we load it with patch( 'ycm.vimsupport.PresentDialog', - return_value = 0 ) as present_dialog: + return_value = 0, + new_callable = ExtendedMock ) as present_dialog: self.server_state.OnFileReadyToParse() assert self.server_state.DiagnosticsForCurrentFileReady() self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ), ] ) - load_extra_conf.assert_has_calls( [ + load_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), ] ) # Subsequent calls don't re-raise the warning self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ) ] ) - load_extra_conf.assert_has_calls( [ + load_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), ] ) @@ -238,36 +243,37 @@ class EventNotification_test( object ): assert self.server_state.DiagnosticsForCurrentFileReady() self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ), ] ) - load_extra_conf.assert_has_calls( [ + load_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), call( FILE_NAME ), ] ) # When the user rejects the extra conf, we reject it with patch( 'ycm.vimsupport.PresentDialog', - return_value = 1 ) as present_dialog: + return_value = 1, + new_callable = ExtendedMock ) as present_dialog: self.server_state.OnFileReadyToParse() assert self.server_state.DiagnosticsForCurrentFileReady() self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ), ] ) - ignore_extra_conf.assert_has_calls( [ + ignore_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), ] ) # Subsequent calls don't re-raise the warning self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ) ] ) - ignore_extra_conf.assert_has_calls( [ + ignore_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), ] ) @@ -276,11 +282,11 @@ class EventNotification_test( object ): assert self.server_state.DiagnosticsForCurrentFileReady() self.server_state.ValidateParseRequest() - present_dialog.assert_has_calls( [ + present_dialog.assert_has_exact_calls( [ PresentDialog_Confirm_Call( MESSAGE ), PresentDialog_Confirm_Call( MESSAGE ), ] ) - ignore_extra_conf.assert_has_calls( [ + ignore_extra_conf.assert_has_exact_calls( [ call( FILE_NAME ), call( FILE_NAME ), ] ) diff --git a/python/ycm/tests/omni_completion_request_tests.py b/python/ycm/tests/omni_completion_request_tests.py new file mode 100644 index 00000000..8d17e07e --- /dev/null +++ b/python/ycm/tests/omni_completion_request_tests.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 YouCompleteMe contributors +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + + +from mock import MagicMock +from nose.tools import eq_ +from hamcrest import assert_that, has_entries + +from ycm.client.omni_completion_request import OmniCompletionRequest + + +def BuildOmnicompletionRequest( results ): + omni_completer = MagicMock() + omni_completer.ComputeCandidates = MagicMock( return_value = results ) + + request = OmniCompletionRequest( omni_completer, None ) + request.Start() + + return request; + + +def Done_AlwaysTrue_test(): + request = BuildOmnicompletionRequest( [] ) + + eq_( request.Done(), True ) + + +def Response_FromOmniCompleter_test(): + results = [ { "word": "test" } ] + request = BuildOmnicompletionRequest( results ) + + eq_( request.Response(), results ) + + +def RawResponse_ConvertedFromOmniCompleter_test(): + vim_results = [ + { "word": "WORD", "abbr": "ABBR", "menu": "MENU", + "kind": "KIND", "info": "INFO" }, + { "word": "WORD2", "abbr": "ABBR2", "menu": "MENU2", + "kind": "KIND2", "info": "INFO" }, + { "word": "WORD", "abbr": "ABBR", }, + { }, + ] + expected_results = [ + has_entries( { "insertion_text": "WORD", "menu_text": "ABBR", + "extra_menu_info": "MENU", "kind": [ "KIND" ], + "detailed_info": "INFO" } ), + has_entries( { "insertion_text": "WORD2", "menu_text": "ABBR2", + "extra_menu_info": "MENU2", "kind": [ "KIND2" ], + "detailed_info": "INFO" } ), + has_entries( { "insertion_text": "WORD", "menu_text": "ABBR", } ), + has_entries( { } ), + ] + request = BuildOmnicompletionRequest( vim_results ) + + results = request.RawResponse() + + eq_( len( results ), len( expected_results ) ) + for result, expected_result in zip( results, expected_results ): + assert_that( result, expected_result ) diff --git a/python/ycm/tests/paths_test.py b/python/ycm/tests/paths_test.py new file mode 100644 index 00000000..d8e5ca7f --- /dev/null +++ b/python/ycm/tests/paths_test.py @@ -0,0 +1,47 @@ +# +# Copyright (C) 2016 YouCompleteMe contributors +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +from ycm.test_utils import MockVimModule +MockVimModule() + +from nose.tools import ok_ +from ycm.paths import EndsWithPython + + +def EndsWithPython_Python2Paths_test(): + python_paths = [ + 'python', + '/usr/bin/python2.6', + '/home/user/.pyenv/shims/python2.7', + r'C:\Python26\python.exe' + ] + + for path in python_paths: + ok_( EndsWithPython( path ) ) + + +def EndsWithPython_NotPython2Paths_test(): + not_python_paths = [ + '/opt/local/bin/vim', + r'C:\Program Files\Vim\vim74\gvim.exe', + '/usr/bin/python3', + '/home/user/.pyenv/shims/python3', + ] + + for path in not_python_paths: + ok_( not EndsWithPython( path ) )