瀏覽代碼

Moved parse request and diagnostics handling to python.

Moved File parse request handling and diagnostic extraction flow into
python to simplify flow and allow easier addition of new parse request
handlers such as semantic highlighter.
Refactored base_test to patch separate vimsupport functions instead of
the whole module, and interfering the test results afterwards.
Added new tests for diagnostic sign place/unplace and error/warning
count extraction API.
Davit Samvelyan 9 年之前
父節點
當前提交
4d97437872

+ 5 - 44
autoload/youcompleteme.vim

@@ -29,14 +29,6 @@ let s:cursor_moved = 0
 let s:moved_vertically_in_insert_mode = 0
 let s:previous_num_chars_on_current_line = strlen( getline('.') )
 
-let s:diagnostic_ui_filetypes = {
-      \ 'cpp': 1,
-      \ 'cs': 1,
-      \ 'c': 1,
-      \ 'objc': 1,
-      \ 'objcpp': 1,
-      \ }
-
 
 function! youcompleteme#Enable()
   " When vim is in diff mode, don't run
@@ -308,11 +300,6 @@ function! s:TurnOffSyntasticForCFamily()
 endfunction
 
 
-function! s:DiagnosticUiSupportedForCurrentFiletype()
-  return get( s:diagnostic_ui_filetypes, &filetype, 0 )
-endfunction
-
-
 function! s:AllowedToCompleteInCurrentFile()
   if empty( &filetype ) ||
         \ getbufvar( winbufnr( winnr() ), "&buftype" ) ==# 'nofile' ||
@@ -462,11 +449,11 @@ function! s:OnFileReadyToParse()
   " happen for special buffers.
   call s:SetUpYcmChangedTick()
 
-  " Order is important here; we need to extract any done diagnostics before
+  " Order is important here; we need to extract any information before
   " reparsing the file again. If we sent the new parse request first, then
   " the response would always be pending when we called
-  " UpdateDiagnosticNotifications.
-  call s:UpdateDiagnosticNotifications()
+  " HandleFileParseRequest.
+  py ycm_state.HandleFileParseRequest()
 
   let buffer_changed = b:changedtick != b:ycm_changedtick.file_ready_to_parse
   if buffer_changed
@@ -612,19 +599,6 @@ function! s:ClosePreviewWindowIfNeeded()
 endfunction
 
 
-function! s:UpdateDiagnosticNotifications()
-  let should_display_diagnostics = g:ycm_show_diagnostics_ui &&
-        \ s:DiagnosticUiSupportedForCurrentFiletype()
-
-  if !should_display_diagnostics
-    py ycm_state.ValidateParseRequest()
-    return
-  endif
-
-  py ycm_state.UpdateDiagnosticInterface()
-endfunction
-
-
 function! s:IdentifierFinishedOperations()
   if !pyeval( 'base.CurrentIdentifierFinished()' )
     return
@@ -853,15 +827,8 @@ function! s:ForceCompile()
 
   echom "Forcing compilation, this will block Vim until done."
   py ycm_state.OnFileReadyToParse()
-  while 1
-    let diagnostics_ready = pyeval(
-          \ 'ycm_state.DiagnosticsForCurrentFileReady()' )
-    if diagnostics_ready
-      break
-    endif
+  py ycm_state.HandleFileParseRequest( True )
 
-    sleep 100m
-  endwhile
   return 1
 endfunction
 
@@ -871,8 +838,6 @@ function! s:ForceCompileAndDiagnostics()
   if !compilation_succeeded
     return
   endif
-
-  call s:UpdateDiagnosticNotifications()
   echom "Diagnostics refreshed."
 endfunction
 
@@ -883,11 +848,7 @@ function! s:ShowDiagnostics()
     return
   endif
 
-  let diags = pyeval(
-        \ 'ycm_state.GetDiagnosticsFromStoredRequest( qflist_format = True )' )
-  if !empty( diags )
-    call setloclist( 0, diags )
-
+  if pyeval( 'ycm_state.PopulateLocationListWithLatestDiagnostics()' )
     if g:ycm_open_loclist_on_ycm_diags
       lopen
     endif

+ 6 - 2
python/ycm/diagnostic_interface.py

@@ -50,6 +50,11 @@ class DiagnosticInterface( object ):
     return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
 
 
+  def PopulateLocationList( self, diags ):
+    vimsupport.SetLocationList(
+      vimsupport.ConvertDiagnosticsToQfList( diags ) )
+
+
   def UpdateWithNewDiagnostics( self, diags ):
     normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags ]
     self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
@@ -65,8 +70,7 @@ class DiagnosticInterface( object ):
       _UpdateSquiggles( self._buffer_number_to_line_to_diags )
 
     if self._user_options[ 'always_populate_location_list' ]:
-      vimsupport.SetLocationList(
-        vimsupport.ConvertDiagnosticsToQfList( normalized_diags ) )
+      self.PopulateLocationList( normalized_diags )
 
   def _EchoDiagnosticForLine( self, line_num ):
     buffer_num = vim.current.buffer.number

+ 124 - 138
python/ycm/tests/base_test.py

@@ -17,262 +17,248 @@
 # You should have received a copy of the GNU General Public License
 # along with YouCompleteMe.  If not, see <http://www.gnu.org/licenses/>.
 
-from nose.tools import eq_, ok_, with_setup
-from mock import MagicMock
+import contextlib
+from nose.tools import eq_, ok_
+from mock import patch
+
 from ycm.test_utils import MockVimModule
 vim_mock = MockVimModule()
 from ycm import base
-from ycm import vimsupport
-import sys
 
-# column is 0-based
-def SetVimCurrentColumnAndLineValue( column, line_value ):
-  vimsupport.CurrentColumn = MagicMock( return_value = column )
-  vimsupport.CurrentLineContents = MagicMock( return_value = line_value )
 
+@contextlib.contextmanager
+def MockCurrentFiletypes( filetypes = [''] ):
+  with patch( 'ycm.vimsupport.CurrentFiletypes', return_value = filetypes ):
+    yield
+
+
+@contextlib.contextmanager
+def MockCurrentColumnAndLineContents( column, line_contents ):
+  with patch( 'ycm.vimsupport.CurrentColumn', return_value = column ):
+    with patch( 'ycm.vimsupport.CurrentLineContents',
+                return_value = line_contents ):
+      yield
 
-def Setup():
-  sys.modules[ 'ycm.vimsupport' ] = MagicMock()
-  vimsupport.CurrentFiletypes = MagicMock( return_value = [''] )
-  vimsupport.CurrentColumn = MagicMock( return_value = 1 )
-  vimsupport.CurrentLineContents = MagicMock( return_value = '' )
+
+@contextlib.contextmanager
+def MockTextAfterCursor( text ):
+  with patch( 'ycm.vimsupport.TextAfterCursor', return_value = text ):
+    yield
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_Basic_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
+  with MockTextAfterCursor( 'bar' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_ParenInTextAfterCursor_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
+  with MockTextAfterCursor( 'bar(zoo' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_PlusInTextAfterCursor_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar+zoo' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
+  with MockTextAfterCursor( 'bar+zoo' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_WhitespaceInTextAfterCursor_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar zoo' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
+  with MockTextAfterCursor( 'bar zoo' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_MoreThanWordMatchingAfterCursor_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar.h' )
-  eq_( [ { 'abbr': 'foobar.h', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar.h' ] ) )
+  with MockTextAfterCursor( 'bar.h' ):
+    eq_( [ { 'abbr': 'foobar.h', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar.h' ] ) )
 
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar(zoo' )
-  eq_( [ { 'abbr': 'foobar(zoo', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText( [ 'foobar(zoo' ] ) )
+  with MockTextAfterCursor( 'bar(zoo' ):
+    eq_( [ { 'abbr': 'foobar(zoo', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText( [ 'foobar(zoo' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_NotSuffix_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
-  eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ],
-       base.AdjustCandidateInsertionText( [ 'foofoo' ] ) )
+  with MockTextAfterCursor( 'bar' ):
+    eq_( [ { 'abbr': 'foofoo', 'word': 'foofoo' } ],
+         base.AdjustCandidateInsertionText( [ 'foofoo' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_NothingAfterCursor_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = '' )
-  eq_( [ 'foofoo',
-         'zobar' ],
-       base.AdjustCandidateInsertionText( [ 'foofoo',
-                                            'zobar' ] ) )
+  with MockTextAfterCursor( '' ):
+    eq_( [ 'foofoo',
+           'zobar' ],
+         base.AdjustCandidateInsertionText( [ 'foofoo',
+                                              'zobar' ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_MultipleStrings_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' },
-         { 'abbr': 'zobar', 'word': 'zo' },
-         { 'abbr': 'qbar', 'word': 'q' },
-         { 'abbr': 'bar', 'word': '' },
-       ],
-       base.AdjustCandidateInsertionText( [ 'foobar',
-                                            'zobar',
-                                            'qbar',
-                                            'bar' ] ) )
-
-
-@with_setup( Setup )
+  with MockTextAfterCursor( 'bar' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' },
+           { 'abbr': 'zobar', 'word': 'zo' },
+           { 'abbr': 'qbar', 'word': 'q' },
+           { 'abbr': 'bar', 'word': '' },
+         ],
+         base.AdjustCandidateInsertionText( [ 'foobar',
+                                              'zobar',
+                                              'qbar',
+                                              'bar' ] ) )
+
+
 def AdjustCandidateInsertionText_DictInput_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
-  eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText(
-         [ { 'word': 'foobar' } ] ) )
+  with MockTextAfterCursor( 'bar' ):
+    eq_( [ { 'abbr': 'foobar', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText(
+           [ { 'word': 'foobar' } ] ) )
 
 
-@with_setup( Setup )
 def AdjustCandidateInsertionText_DontTouchAbbr_test():
-  vimsupport.TextAfterCursor = MagicMock( return_value = 'bar' )
-  eq_( [ { 'abbr': '1234', 'word': 'foo' } ],
-       base.AdjustCandidateInsertionText(
-         [ { 'abbr': '1234', 'word': 'foobar' } ] ) )
+  with MockTextAfterCursor( 'bar' ):
+    eq_( [ { 'abbr': '1234', 'word': 'foo' } ],
+         base.AdjustCandidateInsertionText(
+           [ { 'abbr': '1234', 'word': 'foobar' } ] ) )
 
 
-@with_setup( Setup )
 def OverlapLength_Basic_test():
   eq_( 3, base.OverlapLength( 'foo bar', 'bar zoo' ) )
   eq_( 3, base.OverlapLength( 'foobar', 'barzoo' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_BasicWithUnicode_test():
   eq_( 3, base.OverlapLength( u'bar fäö', u'fäö bar' ) )
   eq_( 3, base.OverlapLength( u'zoofäö', u'fäözoo' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_OneCharOverlap_test():
   eq_( 1, base.OverlapLength( 'foo b', 'b zoo' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_SameStrings_test():
   eq_( 6, base.OverlapLength( 'foobar', 'foobar' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_Substring_test():
   eq_( 6, base.OverlapLength( 'foobar', 'foobarzoo' ) )
   eq_( 6, base.OverlapLength( 'zoofoobar', 'foobar' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_LongestOverlap_test():
   eq_( 7, base.OverlapLength( 'bar foo foo', 'foo foo bar' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_EmptyInput_test():
   eq_( 0, base.OverlapLength( '', 'goobar' ) )
   eq_( 0, base.OverlapLength( 'foobar', '' ) )
   eq_( 0, base.OverlapLength( '', '' ) )
 
 
-@with_setup( Setup )
 def OverlapLength_NoOverlap_test():
   eq_( 0, base.OverlapLength( 'foobar', 'goobar' ) )
   eq_( 0, base.OverlapLength( 'foobar', '(^($@#$#@' ) )
   eq_( 0, base.OverlapLength( 'foo bar zoo', 'foo zoo bar' ) )
 
 
-@with_setup( Setup )
 def LastEnteredCharIsIdentifierChar_Basic_test():
-  SetVimCurrentColumnAndLineValue( 3, 'abc' )
-  ok_( base.LastEnteredCharIsIdentifierChar() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 3, 'abc' ):
+      ok_( base.LastEnteredCharIsIdentifierChar() )
 
-  SetVimCurrentColumnAndLineValue( 2, 'abc' )
-  ok_( base.LastEnteredCharIsIdentifierChar() )
+    with MockCurrentColumnAndLineContents( 2, 'abc' ):
+      ok_( base.LastEnteredCharIsIdentifierChar() )
 
-  SetVimCurrentColumnAndLineValue( 1, 'abc' )
-  ok_( base.LastEnteredCharIsIdentifierChar() )
+    with MockCurrentColumnAndLineContents( 1, 'abc' ):
+      ok_( base.LastEnteredCharIsIdentifierChar() )
 
 
-@with_setup( Setup )
 def LastEnteredCharIsIdentifierChar_FiletypeHtml_test():
-  SetVimCurrentColumnAndLineValue( 3, 'ab-' )
-  vimsupport.CurrentFiletypes = MagicMock( return_value = ['html'] )
-  ok_( base.LastEnteredCharIsIdentifierChar() )
+  with MockCurrentFiletypes( ['html'] ):
+    with MockCurrentColumnAndLineContents( 3, 'ab-' ):
+      ok_( base.LastEnteredCharIsIdentifierChar() )
 
 
-@with_setup( Setup )
 def LastEnteredCharIsIdentifierChar_ColumnIsZero_test():
-  SetVimCurrentColumnAndLineValue( 0, 'abc' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+  with MockCurrentColumnAndLineContents( 0, 'abc' ):
+    ok_( not base.LastEnteredCharIsIdentifierChar() )
 
 
-@with_setup( Setup )
 def LastEnteredCharIsIdentifierChar_LineEmpty_test():
-  SetVimCurrentColumnAndLineValue( 3, '' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 3, '' ):
+      ok_( not base.LastEnteredCharIsIdentifierChar() )
 
-  SetVimCurrentColumnAndLineValue( 0, '' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+    with MockCurrentColumnAndLineContents( 0, '' ):
+      ok_( not base.LastEnteredCharIsIdentifierChar() )
 
 
-@with_setup( Setup )
 def LastEnteredCharIsIdentifierChar_NotIdentChar_test():
-  SetVimCurrentColumnAndLineValue( 3, 'ab;' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 3, 'ab;' ):
+      ok_( not base.LastEnteredCharIsIdentifierChar() )
 
-  SetVimCurrentColumnAndLineValue( 1, ';' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+    with MockCurrentColumnAndLineContents( 1, ';' ):
+      ok_( not base.LastEnteredCharIsIdentifierChar() )
 
-  SetVimCurrentColumnAndLineValue( 3, 'ab-' )
-  ok_( not base.LastEnteredCharIsIdentifierChar() )
+    with MockCurrentColumnAndLineContents( 3, 'ab-' ):
+      ok_( not base.LastEnteredCharIsIdentifierChar() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_Basic_test():
-  SetVimCurrentColumnAndLineValue( 3, 'ab;' )
-  ok_( base.CurrentIdentifierFinished() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 3, 'ab;' ):
+      ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 2, 'ab;' )
-  ok_( not base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 2, 'ab;' ):
+      ok_( not base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 1, 'ab;' )
-  ok_( not base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 1, 'ab;' ):
+      ok_( not base.CurrentIdentifierFinished() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_NothingBeforeColumn_test():
-  SetVimCurrentColumnAndLineValue( 0, 'ab;' )
-  ok_( base.CurrentIdentifierFinished() )
+  with MockCurrentColumnAndLineContents( 0, 'ab;' ):
+    ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 0, '' )
-  ok_( base.CurrentIdentifierFinished() )
+  with MockCurrentColumnAndLineContents( 0, '' ):
+    ok_( base.CurrentIdentifierFinished() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_InvalidColumn_test():
-  SetVimCurrentColumnAndLineValue( 5, '' )
-  ok_( not base.CurrentIdentifierFinished() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 5, '' ):
+      ok_( not base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 5, 'abc' )
-  ok_( not base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 5, 'abc' ):
+      ok_( not base.CurrentIdentifierFinished() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_InMiddleOfLine_test():
-  SetVimCurrentColumnAndLineValue( 4, 'bar.zoo' )
-  ok_( base.CurrentIdentifierFinished() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 4, 'bar.zoo' ):
+      ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 4, 'bar(zoo' )
-  ok_( base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 4, 'bar(zoo' ):
+      ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 4, 'bar-zoo' )
-  ok_( base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 4, 'bar-zoo' ):
+      ok_( base.CurrentIdentifierFinished() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_Html_test():
-  SetVimCurrentColumnAndLineValue( 4, 'bar-zoo' )
-  vimsupport.CurrentFiletypes = MagicMock( return_value = ['html'] )
-  ok_( not base.CurrentIdentifierFinished() )
+  with MockCurrentFiletypes( ['html'] ):
+    with MockCurrentColumnAndLineContents( 4, 'bar-zoo' ):
+      ok_( not base.CurrentIdentifierFinished() )
 
 
-@with_setup( Setup )
 def CurrentIdentifierFinished_WhitespaceOnly_test():
-  SetVimCurrentColumnAndLineValue( 1, '\n' )
-  ok_( base.CurrentIdentifierFinished() )
+  with MockCurrentFiletypes():
+    with MockCurrentColumnAndLineContents( 1, '\n' ):
+      ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 3, '\n    ' )
-  ok_( base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 3, '\n    ' ):
+      ok_( base.CurrentIdentifierFinished() )
 
-  SetVimCurrentColumnAndLineValue( 3, '\t\t\t\t' )
-  ok_( base.CurrentIdentifierFinished() )
+    with MockCurrentColumnAndLineContents( 3, '\t\t\t\t' ):
+      ok_( base.CurrentIdentifierFinished() )
 

+ 134 - 21
python/ycm/tests/event_notification_test.py

@@ -23,9 +23,11 @@ import os
 
 from ycm.youcompleteme import YouCompleteMe
 from ycmd import user_options_store
-from ycmd.responses import UnknownExtraConf
+from ycmd.responses import ( BuildDiagnosticData, Diagnostic, Location, Range,
+                             UnknownExtraConf )
 
 from mock import call, MagicMock, patch
+from nose.tools import eq_, ok_
 
 
 # The default options which are only relevant to the client, not the server and
@@ -34,6 +36,10 @@ from mock import call, MagicMock, patch
 DEFAULT_CLIENT_OPTIONS = {
   'server_log_level': 'info',
   'extra_conf_vim_data': [],
+  'show_diagnostics_ui': 1,
+  'enable_diagnostic_signs': 1,
+  'enable_diagnostic_highlighting': 0,
+  'always_populate_location_list': 0,
 }
 
 
@@ -51,6 +57,17 @@ def PresentDialog_Confirm_Call( message ):
   return call( message, [ 'Ok', 'Cancel' ] )
 
 
+def PlaceSign_Call( sign_id, line_num, buffer_num, is_error ):
+  sign_name = 'YcmError' if is_error else 'YcmWarning'
+  return call( 'sign place {0} line={1} name={2} buffer={3}'
+                  .format( sign_id, line_num, sign_name, buffer_num ) )
+
+
+def UnplaceSign_Call( sign_id, buffer_num ):
+  return call( 'try | exec "sign unplace {0} buffer={1}" |'
+               ' catch /E158/ | endtry'.format( sign_id, buffer_num ) )
+
+
 @contextlib.contextmanager
 def MockArbitraryBuffer( filetype, native_available = True ):
   """Used via the with statement, set up mocked versions of the vim module such
@@ -72,6 +89,12 @@ def MockArbitraryBuffer( filetype, native_available = True ):
       if value == 'getbufvar(0, "&ft")' or value == '&filetype':
         return filetype
 
+      if value.startswith( 'bufnr(' ):
+        return 0
+
+      if value.startswith( 'bufwinnr(' ):
+        return 0
+
       raise ValueError( 'Unexpected evaluation' )
 
     # Arbitrary, but valid, cursor position
@@ -82,6 +105,7 @@ def MockArbitraryBuffer( filetype, native_available = True ):
     current_buffer.number = 0
     current_buffer.filename = os.path.realpath( 'TEST_BUFFER' )
     current_buffer.name = 'TEST_BUFFER'
+    current_buffer.window = 0
 
     # The rest just mock up the Vim module so that our single arbitrary buffer
     # makes sense to vimsupport module.
@@ -144,8 +168,8 @@ class EventNotification_test( object ):
 
   @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
+    # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
+    # in combination with YouCompleteMe.OnFileReadyToParse when the completer
     # raises an exception handling FileReadyToParse event notification
     ERROR_TEXT = 'Some completer response text'
 
@@ -155,8 +179,8 @@ class EventNotification_test( object ):
     with MockArbitraryBuffer( 'javascript' ):
       with MockEventNotification( ErrorResponse ):
         self.server_state.OnFileReadyToParse()
-        assert self.server_state.DiagnosticsForCurrentFileReady()
-        self.server_state.ValidateParseRequest()
+        assert self.server_state.FileParseRequestReady()
+        self.server_state.HandleFileParseRequest()
 
         # The first call raises a warning
         vim_command.assert_has_exact_calls( [
@@ -164,15 +188,15 @@ class EventNotification_test( object ):
         ] )
 
         # Subsequent calls don't re-raise the warning
-        self.server_state.ValidateParseRequest()
+        self.server_state.HandleFileParseRequest()
         vim_command.assert_has_exact_calls( [
           PostVimMessage_Call( ERROR_TEXT ),
         ] )
 
         # But it does if a subsequent event raises again
         self.server_state.OnFileReadyToParse()
-        assert self.server_state.DiagnosticsForCurrentFileReady()
-        self.server_state.ValidateParseRequest()
+        assert self.server_state.FileParseRequestReady()
+        self.server_state.HandleFileParseRequest()
         vim_command.assert_has_exact_calls( [
           PostVimMessage_Call( ERROR_TEXT ),
           PostVimMessage_Call( ERROR_TEXT ),
@@ -184,7 +208,7 @@ class EventNotification_test( object ):
     with MockArbitraryBuffer( 'javascript' ):
       with MockEventNotification( None, False ):
         self.server_state.OnFileReadyToParse()
-        self.server_state.ValidateParseRequest()
+        self.server_state.HandleFileParseRequest()
         vim_command.assert_not_called()
 
 
@@ -198,8 +222,8 @@ class EventNotification_test( object ):
       load_extra_conf,
       *args ):
 
-    # This test validates the behaviour of YouCompleteMe.ValidateParseRequest in
-    # combination with YouCompleteMe.OnFileReadyToParse when the completer
+    # This test validates the behaviour of YouCompleteMe.HandleFileParseRequest
+    # in combination with YouCompleteMe.OnFileReadyToParse when the completer
     # raises the (special) UnknownExtraConf exception
 
     FILE_NAME = 'a_file'
@@ -217,8 +241,8 @@ class EventNotification_test( object ):
                     return_value = 0,
                     new_callable = ExtendedMock ) as present_dialog:
           self.server_state.OnFileReadyToParse()
-          assert self.server_state.DiagnosticsForCurrentFileReady()
-          self.server_state.ValidateParseRequest()
+          assert self.server_state.FileParseRequestReady()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE ),
@@ -228,7 +252,7 @@ class EventNotification_test( object ):
           ] )
 
           # Subsequent calls don't re-raise the warning
-          self.server_state.ValidateParseRequest()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE )
@@ -239,8 +263,8 @@ class EventNotification_test( object ):
 
           # But it does if a subsequent event raises again
           self.server_state.OnFileReadyToParse()
-          assert self.server_state.DiagnosticsForCurrentFileReady()
-          self.server_state.ValidateParseRequest()
+          assert self.server_state.FileParseRequestReady()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE ),
@@ -256,8 +280,8 @@ class EventNotification_test( object ):
                     return_value = 1,
                     new_callable = ExtendedMock ) as present_dialog:
           self.server_state.OnFileReadyToParse()
-          assert self.server_state.DiagnosticsForCurrentFileReady()
-          self.server_state.ValidateParseRequest()
+          assert self.server_state.FileParseRequestReady()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE ),
@@ -267,7 +291,7 @@ class EventNotification_test( object ):
           ] )
 
           # Subsequent calls don't re-raise the warning
-          self.server_state.ValidateParseRequest()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE )
@@ -278,8 +302,8 @@ class EventNotification_test( object ):
 
           # But it does if a subsequent event raises again
           self.server_state.OnFileReadyToParse()
-          assert self.server_state.DiagnosticsForCurrentFileReady()
-          self.server_state.ValidateParseRequest()
+          assert self.server_state.FileParseRequestReady()
+          self.server_state.HandleFileParseRequest()
 
           present_dialog.assert_has_exact_calls( [
             PresentDialog_Confirm_Call( MESSAGE ),
@@ -289,3 +313,92 @@ class EventNotification_test( object ):
             call( FILE_NAME ),
             call( FILE_NAME ),
           ] )
+
+
+  def FileReadyToParse_Diagnostic_Error_Native_test( self ):
+    self._Check_FileReadyToParse_Diagnostic_Error()
+    self._Check_FileReadyToParse_Diagnostic_Warning()
+    self._Check_FileReadyToParse_Diagnostic_Clean()
+
+
+  @patch( 'vim.command' )
+  def _Check_FileReadyToParse_Diagnostic_Error( self, vim_command ):
+    # Tests Vim sign placement and error/warning count python API
+    # when one error is returned.
+    def DiagnosticResponse( *args ):
+      start = Location( 1, 2, 'TEST_BUFFER' )
+      end = Location( 1, 4, 'TEST_BUFFER' )
+      extent = Range( start, end )
+      diagnostic = Diagnostic( [], start, extent, 'expected ;', 'ERROR' )
+      return [ BuildDiagnosticData( diagnostic ) ]
+
+    with MockArbitraryBuffer( 'cpp' ):
+      with MockEventNotification( DiagnosticResponse ):
+        self.server_state.OnFileReadyToParse()
+        ok_( self.server_state.FileParseRequestReady() )
+        self.server_state.HandleFileParseRequest()
+        vim_command.assert_has_calls( [
+          PlaceSign_Call( 1, 1, 0, True )
+        ] )
+        eq_( self.server_state.GetErrorCount(), 1 )
+        eq_( self.server_state.GetWarningCount(), 0 )
+
+        # Consequent calls to HandleFileParseRequest shouldn't mess with
+        # existing diagnostics, when there is no new parse request.
+        vim_command.reset_mock()
+        ok_( not self.server_state.FileParseRequestReady() )
+        self.server_state.HandleFileParseRequest()
+        vim_command.assert_not_called()
+        eq_( self.server_state.GetErrorCount(), 1 )
+        eq_( self.server_state.GetWarningCount(), 0 )
+
+
+  @patch( 'vim.command' )
+  def _Check_FileReadyToParse_Diagnostic_Warning( self, vim_command ):
+    # Tests Vim sign placement/unplacement and error/warning count python API
+    # when one warning is returned.
+    # Should be called after _Check_FileReadyToParse_Diagnostic_Error
+    def DiagnosticResponse( *args ):
+      start = Location( 2, 2, 'TEST_BUFFER' )
+      end = Location( 2, 4, 'TEST_BUFFER' )
+      extent = Range( start, end )
+      diagnostic = Diagnostic( [], start, extent, 'cast', 'WARNING' )
+      return [ BuildDiagnosticData( diagnostic ) ]
+
+    with MockArbitraryBuffer( 'cpp' ):
+      with MockEventNotification( DiagnosticResponse ):
+        self.server_state.OnFileReadyToParse()
+        ok_( self.server_state.FileParseRequestReady() )
+        self.server_state.HandleFileParseRequest()
+        vim_command.assert_has_calls( [
+          PlaceSign_Call( 2, 2, 0, False ),
+          UnplaceSign_Call( 1, 0 )
+        ] )
+        eq_( self.server_state.GetErrorCount(), 0 )
+        eq_( self.server_state.GetWarningCount(), 1 )
+
+        # Consequent calls to HandleFileParseRequest shouldn't mess with
+        # existing diagnostics, when there is no new parse request.
+        vim_command.reset_mock()
+        ok_( not self.server_state.FileParseRequestReady() )
+        self.server_state.HandleFileParseRequest()
+        vim_command.assert_not_called()
+        eq_( self.server_state.GetErrorCount(), 0 )
+        eq_( self.server_state.GetWarningCount(), 1 )
+
+
+  @patch( 'vim.command' )
+  def _Check_FileReadyToParse_Diagnostic_Clean( self, vim_command ):
+    # Tests Vim sign unplacement and error/warning count python API
+    # when there are no errors/warnings left.
+    # Should be called after _Check_FileReadyToParse_Diagnostic_Warning
+    with MockArbitraryBuffer( 'cpp' ):
+      with MockEventNotification( MagicMock( return_value = [] ) ):
+        self.server_state.OnFileReadyToParse()
+        self.server_state.HandleFileParseRequest()
+        vim_command.assert_has_calls( [
+          UnplaceSign_Call( 2, 0 )
+        ] )
+        eq_( self.server_state.GetErrorCount(), 0 )
+        eq_( self.server_state.GetWarningCount(), 0 )
+

+ 44 - 34
python/ycm/youcompleteme.py

@@ -75,6 +75,7 @@ SERVER_CRASH_MESSAGE_STDERR_FILE_DELETED = (
   "Logfile was deleted; set 'g:ycm_server_keep_logfiles' to see errors "
   "in the future." )
 SERVER_IDLE_SUICIDE_SECONDS = 10800  # 3 hours
+DIAGNOSTIC_UI_FILETYPES = set( [ 'cpp', 'cs', 'c', 'objc', 'objcpp' ] )
 
 
 class YouCompleteMe( object ):
@@ -85,6 +86,7 @@ class YouCompleteMe( object ):
     self._omnicomp = OmniCompleter( user_options )
     self._latest_file_parse_request = None
     self._latest_completion_request = None
+    self._latest_diagnostics = []
     self._server_stdout = None
     self._server_stderr = None
     self._server_popen = None
@@ -447,56 +449,64 @@ class YouCompleteMe( object ):
       return None
     return completion[ "extra_data" ][ "required_namespace_import" ]
 
+
   def GetErrorCount( self ):
     return self._diag_interface.GetErrorCount()
 
+
   def GetWarningCount( self ):
     return self._diag_interface.GetWarningCount()
 
-  def DiagnosticsForCurrentFileReady( self ):
-    return bool( self._latest_file_parse_request and
-                 self._latest_file_parse_request.Done() )
 
+  def DiagnosticUiSupportedForCurrentFiletype( self ):
+    return any( [ x in DIAGNOSTIC_UI_FILETYPES
+                  for x in vimsupport.CurrentFiletypes() ] )
+
+
+  def ShouldDisplayDiagnostics( self ):
+    return bool( self._user_options[ 'show_diagnostics_ui' ] and
+                 self.DiagnosticUiSupportedForCurrentFiletype() )
 
-  def GetDiagnosticsFromStoredRequest( self, qflist_format = False ):
-    if self.DiagnosticsForCurrentFileReady():
-      diagnostics = self._latest_file_parse_request.Response()
-      # We set the diagnostics request to None because we want to prevent
-      # repeated refreshing of the buffer with the same diags. Setting this to
-      # None makes DiagnosticsForCurrentFileReady return False until the next
-      # request is created.
-      self._latest_file_parse_request = None
-      if qflist_format:
-        return vimsupport.ConvertDiagnosticsToQfList( diagnostics )
-      else:
-        return diagnostics
-    return []
+
+  def PopulateLocationListWithLatestDiagnostics( self ):
+    # Do nothing if loc list is already populated by diag_interface
+    if not self._user_options[ 'always_populate_location_list' ]:
+      self._diag_interface.PopulateLocationList( self._latest_diagnostics )
+    return bool( self._latest_diagnostics )
 
 
   def UpdateDiagnosticInterface( self ):
-    if ( self.DiagnosticsForCurrentFileReady() and
-         self.NativeFiletypeCompletionUsable() ):
-      self._diag_interface.UpdateWithNewDiagnostics(
-        self.GetDiagnosticsFromStoredRequest() )
+    self._diag_interface.UpdateWithNewDiagnostics( self._latest_diagnostics )
 
 
-  def ValidateParseRequest( self ):
-    if ( self.DiagnosticsForCurrentFileReady() and
-         self.NativeFiletypeCompletionUsable() ):
+  def FileParseRequestReady( self, block = False ):
+    return bool( self._latest_file_parse_request and
+                 ( block or self._latest_file_parse_request.Done() ) )
 
-      # YCM client has a hard-coded list of filetypes which are known to support
-      # diagnostics. These are found in autoload/youcompleteme.vim in
-      # s:diagnostic_ui_filetypes.
-      #
-      # For filetypes which don't support diagnostics, we just want to check the
-      # _latest_file_parse_request for any exception or UnknownExtraConf
-      # response, to allow the server to raise configuration warnings, etc.
-      # to the user. We ignore any other supplied data.
-      self._latest_file_parse_request.Response()
 
-      # We set the diagnostics request to None because we want to prevent
+  def HandleFileParseRequest( self, block = False ):
+    # Order is important here:
+    # FileParseRequestReady has a low cost, while
+    # NativeFiletypeCompletionUsable is a blocking server request
+    if ( self.FileParseRequestReady( block ) and
+         self.NativeFiletypeCompletionUsable() ):
+
+      if self.ShouldDisplayDiagnostics():
+        self._latest_diagnostics = self._latest_file_parse_request.Response()
+        self.UpdateDiagnosticInterface()
+      else:
+        # YCM client has a hard-coded list of filetypes which are known
+        # to support diagnostics, self.DiagnosticUiSupportedForCurrentFiletype()
+        #
+        # For filetypes which don't support diagnostics, we just want to check
+        # the _latest_file_parse_request for any exception or UnknownExtraConf
+        # response, to allow the server to raise configuration warnings, etc.
+        # to the user. We ignore any other supplied data.
+        self._latest_file_parse_request.Response()
+
+      # We set the file parse request to None because we want to prevent
       # repeated issuing of the same warnings/errors/prompts. Setting this to
-      # None makes DiagnosticsForCurrentFileReady return False until the next
+      # None makes FileParseRequestReady return False until the next
       # request is created.
       #
       # Note: it is the server's responsibility to determine the frequency of