1
0

diagnostic_interface.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. from future import standard_library
  22. standard_library.install_aliases()
  23. from builtins import * # noqa
  24. from future.utils import itervalues, iteritems
  25. from collections import defaultdict, namedtuple
  26. from ycm import vimsupport
  27. import vim
  28. class DiagnosticInterface( object ):
  29. def __init__( self, user_options ):
  30. self._user_options = user_options
  31. # Line and column numbers are 1-based
  32. self._buffer_number_to_line_to_diags = defaultdict(
  33. lambda: defaultdict( list ) )
  34. self._next_sign_id = 1
  35. self._previous_line_number = -1
  36. self._diag_message_needs_clearing = False
  37. self._placed_signs = []
  38. def OnCursorMoved( self ):
  39. line, _ = vimsupport.CurrentLineAndColumn()
  40. line += 1 # Convert to 1-based
  41. if line != self._previous_line_number:
  42. self._previous_line_number = line
  43. if self._user_options[ 'echo_current_diagnostic' ]:
  44. self._EchoDiagnosticForLine( line )
  45. def GetErrorCount( self ):
  46. return len( self._FilterDiagnostics( _DiagnosticIsError ) )
  47. def GetWarningCount( self ):
  48. return len( self._FilterDiagnostics( _DiagnosticIsWarning ) )
  49. def PopulateLocationList( self, diags ):
  50. vimsupport.SetLocationList(
  51. vimsupport.ConvertDiagnosticsToQfList( diags ) )
  52. def UpdateWithNewDiagnostics( self, diags ):
  53. normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags ]
  54. self._buffer_number_to_line_to_diags = _ConvertDiagListToDict(
  55. normalized_diags )
  56. if self._user_options[ 'enable_diagnostic_signs' ]:
  57. self._placed_signs, self._next_sign_id = _UpdateSigns(
  58. self._placed_signs,
  59. self._buffer_number_to_line_to_diags,
  60. self._next_sign_id )
  61. if self._user_options[ 'enable_diagnostic_highlighting' ]:
  62. _UpdateSquiggles( self._buffer_number_to_line_to_diags )
  63. if self._user_options[ 'always_populate_location_list' ]:
  64. self.PopulateLocationList( normalized_diags )
  65. def _EchoDiagnosticForLine( self, line_num ):
  66. buffer_num = vim.current.buffer.number
  67. diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ]
  68. if not diags:
  69. if self._diag_message_needs_clearing:
  70. # Clear any previous diag echo
  71. vimsupport.EchoText( '', False )
  72. self._diag_message_needs_clearing = False
  73. return
  74. text = diags[ 0 ][ 'text' ]
  75. if diags[ 0 ].get( 'fixit_available', False ):
  76. text += ' (FixIt)'
  77. vimsupport.EchoTextVimWidth( text )
  78. self._diag_message_needs_clearing = True
  79. def _FilterDiagnostics( self, predicate ):
  80. matched_diags = []
  81. line_to_diags = self._buffer_number_to_line_to_diags[
  82. vim.current.buffer.number ]
  83. for diags in itervalues( line_to_diags ):
  84. matched_diags.extend( list( filter( predicate, diags ) ) )
  85. return matched_diags
  86. def _UpdateSquiggles( buffer_number_to_line_to_diags ):
  87. vimsupport.ClearYcmSyntaxMatches()
  88. line_to_diags = buffer_number_to_line_to_diags[ vim.current.buffer.number ]
  89. for diags in itervalues( line_to_diags ):
  90. for diag in diags:
  91. location_extent = diag[ 'location_extent' ]
  92. is_error = _DiagnosticIsError( diag )
  93. if location_extent[ 'start' ][ 'line_num' ] < 0:
  94. location = diag[ 'location' ]
  95. vimsupport.AddDiagnosticSyntaxMatch(
  96. location[ 'line_num' ],
  97. location[ 'column_num' ] )
  98. else:
  99. vimsupport.AddDiagnosticSyntaxMatch(
  100. location_extent[ 'start' ][ 'line_num' ],
  101. location_extent[ 'start' ][ 'column_num' ],
  102. location_extent[ 'end' ][ 'line_num' ],
  103. location_extent[ 'end' ][ 'column_num' ],
  104. is_error = is_error )
  105. for diag_range in diag[ 'ranges' ]:
  106. vimsupport.AddDiagnosticSyntaxMatch(
  107. diag_range[ 'start' ][ 'line_num' ],
  108. diag_range[ 'start' ][ 'column_num' ],
  109. diag_range[ 'end' ][ 'line_num' ],
  110. diag_range[ 'end' ][ 'column_num' ],
  111. is_error = is_error )
  112. def _UpdateSigns( placed_signs, buffer_number_to_line_to_diags, next_sign_id ):
  113. new_signs, kept_signs, next_sign_id = _GetKeptAndNewSigns(
  114. placed_signs, buffer_number_to_line_to_diags, next_sign_id
  115. )
  116. # Dummy sign used to prevent "flickering" in Vim when last mark gets
  117. # deleted from buffer. Dummy sign prevents Vim to collapsing the sign column
  118. # in that case.
  119. # There's also a vim bug which causes the whole window to redraw in some
  120. # conditions (vim redraw logic is very complex). But, somehow, if we place a
  121. # dummy sign before placing other "real" signs, it will not redraw the
  122. # buffer (patch to vim pending).
  123. dummy_sign_needed = not kept_signs and new_signs
  124. if dummy_sign_needed:
  125. vimsupport.PlaceDummySign( next_sign_id + 1,
  126. vim.current.buffer.number,
  127. new_signs[ 0 ].line )
  128. # We place only those signs that haven't been placed yet.
  129. new_placed_signs = _PlaceNewSigns( kept_signs, new_signs )
  130. # We use incremental placement, so signs that already placed on the correct
  131. # lines will not be deleted and placed again, which should improve performance
  132. # in case of many diags. Signs which don't exist in the current diag should be
  133. # deleted.
  134. _UnplaceObsoleteSigns( kept_signs, placed_signs )
  135. if dummy_sign_needed:
  136. vimsupport.UnPlaceDummySign( next_sign_id + 1, vim.current.buffer.number )
  137. return new_placed_signs, next_sign_id
  138. def _GetKeptAndNewSigns( placed_signs, buffer_number_to_line_to_diags,
  139. next_sign_id ):
  140. new_signs = []
  141. kept_signs = []
  142. for buffer_number, line_to_diags in iteritems( buffer_number_to_line_to_diags ):
  143. if not vimsupport.BufferIsVisible( buffer_number ):
  144. continue
  145. for line, diags in iteritems( line_to_diags ):
  146. for diag in diags:
  147. sign = _DiagSignPlacement( next_sign_id,
  148. line,
  149. buffer_number,
  150. _DiagnosticIsError( diag ) )
  151. if sign not in placed_signs:
  152. new_signs += [ sign ]
  153. next_sign_id += 1
  154. else:
  155. # We use .index here because `sign` contains a new id, but
  156. # we need the sign with the old id to unplace it later on.
  157. # We won't be placing the new sign.
  158. kept_signs += [ placed_signs[ placed_signs.index( sign ) ] ]
  159. return new_signs, kept_signs, next_sign_id
  160. def _PlaceNewSigns( kept_signs, new_signs ):
  161. placed_signs = kept_signs[:]
  162. for sign in new_signs:
  163. # Do not set two signs on the same line, it will screw up storing sign
  164. # locations.
  165. if sign in placed_signs:
  166. continue
  167. vimsupport.PlaceSign( sign.id, sign.line, sign.buffer, sign.is_error )
  168. placed_signs.append(sign)
  169. return placed_signs
  170. def _UnplaceObsoleteSigns( kept_signs, placed_signs ):
  171. for sign in placed_signs:
  172. if sign not in kept_signs:
  173. vimsupport.UnplaceSignInBuffer( sign.buffer, sign.id )
  174. def _ConvertDiagListToDict( diag_list ):
  175. buffer_to_line_to_diags = defaultdict( lambda: defaultdict( list ) )
  176. for diag in diag_list:
  177. location = diag[ 'location' ]
  178. buffer_number = vimsupport.GetBufferNumberForFilename(
  179. location[ 'filepath' ] )
  180. line_number = location[ 'line_num' ]
  181. buffer_to_line_to_diags[ buffer_number ][ line_number ].append( diag )
  182. for line_to_diags in itervalues( buffer_to_line_to_diags ):
  183. for diags in itervalues( line_to_diags ):
  184. # We also want errors to be listed before warnings so that errors aren't
  185. # hidden by the warnings; Vim won't place a sign oven an existing one.
  186. diags.sort( key = lambda diag: ( diag[ 'location' ][ 'column_num' ],
  187. diag[ 'kind' ] ) )
  188. return buffer_to_line_to_diags
  189. def _DiagnosticIsError( diag ):
  190. return diag[ 'kind' ] == 'ERROR'
  191. def _DiagnosticIsWarning( diag ):
  192. return diag[ 'kind' ] == 'WARNING'
  193. def _NormalizeDiagnostic( diag ):
  194. def ClampToOne( value ):
  195. return value if value > 0 else 1
  196. location = diag[ 'location' ]
  197. location[ 'column_num' ] = ClampToOne( location[ 'column_num' ] )
  198. location[ 'line_num' ] = ClampToOne( location[ 'line_num' ] )
  199. return diag
  200. class _DiagSignPlacement( namedtuple( "_DiagSignPlacement",
  201. [ 'id', 'line', 'buffer', 'is_error' ] ) ):
  202. # We want two signs that have different ids but the same location to compare
  203. # equal. ID doesn't matter.
  204. def __eq__( self, other ):
  205. return ( self.line == other.line and
  206. self.buffer == other.buffer and
  207. self.is_error == other.is_error )