diagnostic_interface.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. # Copyright (C) 2013 Google Inc.
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. from __future__ import unicode_literals
  18. from __future__ import print_function
  19. from __future__ import division
  20. from __future__ import absolute_import
  21. # Not installing aliases from python-future; it's unreliable and slow.
  22. from builtins import * # noqa
  23. from future.utils import itervalues, iteritems
  24. from collections import defaultdict, namedtuple
  25. from ycm import vimsupport
  26. from ycm.diagnostic_filter import DiagnosticFilter, CompileLevel
  27. import vim
  28. class DiagnosticInterface( object ):
  29. def __init__( self, user_options ):
  30. self._user_options = user_options
  31. self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
  32. # Line and column numbers are 1-based
  33. self._buffer_number_to_line_to_diags = defaultdict(
  34. lambda: defaultdict( list ) )
  35. self._next_sign_id = 1
  36. self._previous_line_number = -1
  37. self._diag_message_needs_clearing = False
  38. self._placed_signs = []
  39. def OnCursorMoved( self ):
  40. line, _ = vimsupport.CurrentLineAndColumn()
  41. line += 1 # Convert to 1-based
  42. if line != self._previous_line_number:
  43. self._previous_line_number = line
  44. if self._user_options[ 'echo_current_diagnostic' ]:
  45. self._EchoDiagnosticForLine( line )
  46. def GetErrorCount( self ):
  47. return len( self._FilterDiagnostics( _DiagnosticIsError ) )
  48. def GetWarningCount( self ):
  49. return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
  50. def PopulateLocationList( self, diags ):
  51. vimsupport.SetLocationList(
  52. vimsupport.ConvertDiagnosticsToQfList(
  53. self._ApplyDiagnosticFilter( diags ) ) )
  54. def UpdateWithNewDiagnostics( self, diags ):
  55. normalized_diags = [ _NormalizeDiagnostic( x ) for x in
  56. self._ApplyDiagnosticFilter( diags ) ]
  57. self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
  58. normalized_diags )
  59. if self._user_options[ 'enable_diagnostic_signs' ]:
  60. self._placed_signs, self._next_sign_id = _UpdateSigns(
  61. self._placed_signs,
  62. self._buffer_number_to_line_to_diags,
  63. self._next_sign_id )
  64. if self._user_options[ 'enable_diagnostic_highlighting' ]:
  65. _UpdateSquiggles( self._buffer_number_to_line_to_diags )
  66. if self._user_options[ 'always_populate_location_list' ]:
  67. self.PopulateLocationList( normalized_diags )
  68. def _ApplyDiagnosticFilter( self, diags, extra_predicate = None ):
  69. filetypes = vimsupport.CurrentFiletypes()
  70. diag_filter = self._diag_filter.SubsetForTypes( filetypes )
  71. predicate = diag_filter.IsAllowed
  72. if extra_predicate is not None:
  73. def Filter( diag ):
  74. return extra_predicate( diag ) and diag_filter.IsAllowed( diag )
  75. predicate = Filter
  76. return filter( predicate, diags )
  77. def _EchoDiagnosticForLine( self, line_num ):
  78. buffer_num = vim.current.buffer.number
  79. diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ]
  80. if not diags:
  81. if self._diag_message_needs_clearing:
  82. # Clear any previous diag echo
  83. vimsupport.PostVimMessage( '', warning = False )
  84. self._diag_message_needs_clearing = False
  85. return
  86. text = diags[ 0 ][ 'text' ]
  87. if diags[ 0 ].get( 'fixit_available', False ):
  88. text += ' (FixIt)'
  89. vimsupport.PostVimMessage( text, warning = False, truncate = True )
  90. self._diag_message_needs_clearing = True
  91. def _FilterDiagnostics( self, predicate ):
  92. matched_diags = []
  93. line_to_diags = self._buffer_number_to_line_to_diags[
  94. vim.current.buffer.number ]
  95. for diags in itervalues( line_to_diags ):
  96. matched_diags.extend( list(
  97. self._ApplyDiagnosticFilter( diags, predicate ) ) )
  98. return matched_diags
  99. def _UpdateSquiggles( buffer_number_to_line_to_diags ):
  100. vimsupport.ClearYcmSyntaxMatches()
  101. line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ]
  102. for diags in itervalues( line_to_diags ):
  103. for diag in diags:
  104. location_extent = diag[ 'location_extent' ]
  105. is_error = _DiagnosticIsError( diag )
  106. if location_extent[ 'start' ][ 'line_num' ] < 0:
  107. location = diag[ 'location' ]
  108. vimsupport.AddDiagnosticSyntaxMatch(
  109. location[ 'line_num' ],
  110. location[ 'column_num' ] )
  111. else:
  112. vimsupport.AddDiagnosticSyntaxMatch(
  113. location_extent[ 'start' ][ 'line_num' ],
  114. location_extent[ 'start' ][ 'column_num' ],
  115. location_extent[ 'end' ][ 'line_num' ],
  116. location_extent[ 'end' ][ 'column_num' ],
  117. is_error = is_error )
  118. for diag_range in diag[ 'ranges' ]:
  119. vimsupport.AddDiagnosticSyntaxMatch(
  120. diag_range[ 'start' ][ 'line_num' ],
  121. diag_range[ 'start' ][ 'column_num' ],
  122. diag_range[ 'end' ][ 'line_num' ],
  123. diag_range[ 'end' ][ 'column_num' ],
  124. is_error = is_error )
  125. def _UpdateSigns( placed_signs, buffer_number_to_line_to_diags, next_sign_id ):
  126. new_signs, kept_signs, next_sign_id = _GetKeptAndNewSigns(
  127. placed_signs, buffer_number_to_line_to_diags, next_sign_id
  128. )
  129. # Dummy sign used to prevent "flickering" in Vim when last mark gets
  130. # deleted from buffer. Dummy sign prevents Vim to collapsing the sign column
  131. # in that case.
  132. # There's also a vim bug which causes the whole window to redraw in some
  133. # conditions (vim redraw logic is very complex). But, somehow, if we place a
  134. # dummy sign before placing other "real" signs, it will not redraw the
  135. # buffer (patch to vim pending).
  136. dummy_sign_needed = not kept_signs and new_signs
  137. if dummy_sign_needed:
  138. vimsupport.PlaceDummySign( next_sign_id + 1,
  139. vim.current.buffer.number,
  140. new_signs[ 0 ].line )
  141. # We place only those signs that haven't been placed yet.
  142. new_placed_signs = _PlaceNewSigns( kept_signs, new_signs )
  143. # We use incremental placement, so signs that already placed on the correct
  144. # lines will not be deleted and placed again, which should improve performance
  145. # in case of many diags. Signs which don't exist in the current diag should be
  146. # deleted.
  147. _UnplaceObsoleteSigns( kept_signs, placed_signs )
  148. if dummy_sign_needed:
  149. vimsupport.UnPlaceDummySign( next_sign_id + 1, vim.current.buffer.number )
  150. return new_placed_signs, next_sign_id
  151. def _GetKeptAndNewSigns( placed_signs, buffer_number_to_line_to_diags,
  152. next_sign_id ):
  153. new_signs = []
  154. kept_signs = []
  155. for buffer_number, line_to_diags in iteritems(
  156. buffer_number_to_line_to_diags ):
  157. if not vimsupport.BufferIsVisible( buffer_number ):
  158. continue
  159. for line, diags in iteritems( line_to_diags ):
  160. for diag in diags:
  161. sign = _DiagSignPlacement( next_sign_id,
  162. line,
  163. buffer_number,
  164. _DiagnosticIsError( diag ) )
  165. if sign not in placed_signs:
  166. new_signs += [ sign ]
  167. next_sign_id += 1
  168. else:
  169. # We use .index here because `sign` contains a new id, but
  170. # we need the sign with the old id to unplace it later on.
  171. # We won't be placing the new sign.
  172. kept_signs += [ placed_signs[ placed_signs.index( sign ) ] ]
  173. return new_signs, kept_signs, next_sign_id
  174. def _PlaceNewSigns( kept_signs, new_signs ):
  175. placed_signs = kept_signs[:]
  176. for sign in new_signs:
  177. # Do not set two signs on the same line, it will screw up storing sign
  178. # locations.
  179. if sign in placed_signs:
  180. continue
  181. vimsupport.PlaceSign( sign.id, sign.line, sign.buffer, sign.is_error )
  182. placed_signs.append(sign)
  183. return placed_signs
  184. def _UnplaceObsoleteSigns( kept_signs, placed_signs ):
  185. for sign in placed_signs:
  186. if sign not in kept_signs:
  187. vimsupport.UnplaceSignInBuffer( sign.buffer, sign.id )
  188. def _ConvertDiagListToDict( diag_list ):
  189. buffer_to_line_to_diags = defaultdict( lambda: defaultdict( list ) )
  190. for diag in diag_list:
  191. location = diag[ 'location' ]
  192. buffer_number = vimsupport.GetBufferNumberForFilename(
  193. location[ 'filepath' ] )
  194. line_number = location[ 'line_num' ]
  195. buffer_to_line_to_diags[ buffer_number ][ line_number ].append( diag )
  196. for line_to_diags in itervalues( buffer_to_line_to_diags ):
  197. for diags in itervalues( line_to_diags ):
  198. # We also want errors to be listed before warnings so that errors aren't
  199. # hidden by the warnings; Vim won't place a sign oven an existing one.
  200. diags.sort( key = lambda diag: ( diag[ 'location' ][ 'column_num' ],
  201. diag[ 'kind' ] ) )
  202. return buffer_to_line_to_diags
  203. _DiagnosticIsError = CompileLevel( 'error' )
  204. _DiagnosticIsWarning = CompileLevel( 'warning' )
  205. def _NormalizeDiagnostic( diag ):
  206. def ClampToOne( value ):
  207. return value if value > 0 else 1
  208. location = diag[ 'location' ]
  209. location[ 'column_num' ] = ClampToOne( location[ 'column_num' ] )
  210. location[ 'line_num' ] = ClampToOne( location[ 'line_num' ] )
  211. return diag
  212. class _DiagSignPlacement(
  213. namedtuple( "_DiagSignPlacement",
  214. [ 'id', 'line', 'buffer', 'is_error' ] ) ):
  215. # We want two signs that have different ids but the same location to compare
  216. # equal. ID doesn't matter.
  217. def __eq__( self, other ):
  218. return ( self.line == other.line and
  219. self.buffer == other.buffer and
  220. self.is_error == other.is_error )