diagnostic_interface.py 8.8 KB

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