diagnostic_interface.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # Copyright (C) 2013-2018 YouCompleteMe contributors
  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
  18. from ycm import vimsupport
  19. from ycm.diagnostic_filter import DiagnosticFilter, CompileLevel
  20. import vim
  21. YCM_VIM_PROPERTY_ID = 0
  22. class DiagnosticInterface:
  23. def __init__( self, bufnr, user_options ):
  24. self._bufnr = bufnr
  25. self._user_options = user_options
  26. self._diagnostics = []
  27. self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
  28. # Line and column numbers are 1-based
  29. self._line_to_diags = defaultdict( list )
  30. self._previous_diag_line_number = -1
  31. self._diag_message_needs_clearing = None
  32. def OnCursorMoved( self ):
  33. if self._user_options[ 'echo_current_diagnostic' ]:
  34. line, _ = vimsupport.CurrentLineAndColumn()
  35. line += 1 # Convert to 1-based
  36. if line != self._previous_diag_line_number:
  37. self._EchoDiagnosticForLine( line )
  38. def GetErrorCount( self ):
  39. return self._DiagnosticsCount( _DiagnosticIsError )
  40. def GetWarningCount( self ):
  41. return self._DiagnosticsCount( _DiagnosticIsWarning )
  42. def PopulateLocationList( self, open_on_edit = False ):
  43. # Do nothing if loc list is already populated by diag_interface
  44. if not self._user_options[ 'always_populate_location_list' ]:
  45. self._UpdateLocationLists( open_on_edit )
  46. return bool( self._diagnostics )
  47. def UpdateWithNewDiagnostics( self, diags, open_on_edit = False ):
  48. self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
  49. self._ApplyDiagnosticFilter( diags ) ]
  50. self._ConvertDiagListToDict()
  51. if ( self._user_options[ 'update_diagnostics_in_insert_mode' ] or
  52. 'i' not in vim.eval( 'mode()' ) ):
  53. self.RefreshDiagnosticsUI( open_on_edit )
  54. def RefreshDiagnosticsUI( self, open_on_edit = False ):
  55. if self._user_options[ 'echo_current_diagnostic' ]:
  56. self._EchoDiagnostic()
  57. if self._user_options[ 'enable_diagnostic_signs' ]:
  58. self._UpdateSigns()
  59. self.UpdateMatches()
  60. if self._user_options[ 'always_populate_location_list' ]:
  61. self._UpdateLocationLists( open_on_edit )
  62. def DiagnosticsForLine( self, line_number ):
  63. return self._line_to_diags[ line_number ]
  64. def _ApplyDiagnosticFilter( self, diags ):
  65. filetypes = vimsupport.GetBufferFiletypes( self._bufnr )
  66. diag_filter = self._diag_filter.SubsetForTypes( filetypes )
  67. return filter( diag_filter.IsAllowed, diags )
  68. def _EchoDiagnostic( self ):
  69. line, _ = vimsupport.CurrentLineAndColumn()
  70. line += 1 # Convert to 1-based
  71. self._EchoDiagnosticForLine( line )
  72. def _EchoDiagnosticForLine( self, line_num ):
  73. global YCM_VIM_PROPERTY_ID
  74. if self._diag_message_needs_clearing is not None:
  75. # Clear any previous diag echo
  76. vimsupport.RemoveTextProperty( **self._diag_message_needs_clearing )
  77. self._diag_message_needs_clearing = None
  78. self._previous_diag_line_number = line_num
  79. diags = self._line_to_diags[ line_num ]
  80. if not diags:
  81. return
  82. first_diag = diags[ 0 ]
  83. text = first_diag[ 'text' ]
  84. if first_diag.get( 'fixit_available', False ):
  85. text += ' (FixIt)'
  86. self._diag_message_needs_clearing = {
  87. 'buffer_number': self._bufnr,
  88. 'prop_id': vimsupport.AddTextProperty(
  89. self._bufnr,
  90. line_num,
  91. 0,
  92. 'YcmErrorProperty',
  93. {
  94. 'text': ' ' + ' '.join( text.splitlines() ),
  95. 'text_align': 'right',
  96. 'text_wrap': 'wrap'
  97. } ),
  98. 'line_num': line_num,
  99. 'prop_type': 'YcmErrorProperty'
  100. }
  101. def _DiagnosticsCount( self, predicate ):
  102. count = 0
  103. for diags in self._line_to_diags.values():
  104. count += sum( 1 for d in diags if predicate( d ) )
  105. return count
  106. def _UpdateLocationLists( self, open_on_edit = False ):
  107. vimsupport.SetLocationListsForBuffer(
  108. self._bufnr,
  109. vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ),
  110. open_on_edit )
  111. def UpdateMatches( self ):
  112. if not self._user_options[ 'enable_diagnostic_highlighting' ]:
  113. return
  114. props_to_remove = vimsupport.GetTextProperties( self._bufnr )
  115. for diags in self._line_to_diags.values():
  116. # Insert squiggles in reverse order so that errors overlap warnings.
  117. for diag in reversed( diags ):
  118. for line, column, name, extras in _ConvertDiagnosticToTextProperties(
  119. self._bufnr,
  120. diag ):
  121. global YCM_VIM_PROPERTY_ID
  122. # Note the following .remove() works because the __eq__ on
  123. # DiagnosticProperty does not actually check the IDs match...
  124. diag_prop = vimsupport.DiagnosticProperty(
  125. YCM_VIM_PROPERTY_ID,
  126. name,
  127. line,
  128. column,
  129. extras[ 'end_col' ] - column if 'end_col' in extras else column )
  130. try:
  131. props_to_remove.remove( diag_prop )
  132. except ValueError:
  133. extras.update( {
  134. 'id': YCM_VIM_PROPERTY_ID
  135. } )
  136. vimsupport.AddTextProperty( self._bufnr,
  137. line,
  138. column,
  139. name,
  140. extras )
  141. YCM_VIM_PROPERTY_ID += 1
  142. for prop in props_to_remove:
  143. vimsupport.RemoveDiagnosticProperty( self._bufnr, prop )
  144. def _UpdateSigns( self ):
  145. signs_to_unplace = vimsupport.GetSignsInBuffer( self._bufnr )
  146. signs_to_place = []
  147. for line, diags in self._line_to_diags.items():
  148. if not diags:
  149. continue
  150. # We always go for the first diagnostic on the line because diagnostics
  151. # are sorted by errors in priority and Vim can only display one sign by
  152. # line.
  153. name = 'YcmError' if _DiagnosticIsError( diags[ 0 ] ) else 'YcmWarning'
  154. sign = {
  155. 'lnum': line,
  156. 'name': name,
  157. 'buffer': self._bufnr,
  158. 'group': 'ycm_signs'
  159. }
  160. try:
  161. signs_to_unplace.remove( sign )
  162. except ValueError:
  163. signs_to_place.append( sign )
  164. vim.eval( f'sign_placelist( { signs_to_place } )' )
  165. vim.eval( f'sign_unplacelist( { signs_to_unplace } )' )
  166. def _ConvertDiagListToDict( self ):
  167. self._line_to_diags = defaultdict( list )
  168. for diag in self._diagnostics:
  169. location = diag[ 'location' ]
  170. bufnr = vimsupport.GetBufferNumberForFilename( location[ 'filepath' ] )
  171. if bufnr == self._bufnr:
  172. line_number = location[ 'line_num' ]
  173. self._line_to_diags[ line_number ].append( diag )
  174. for diags in self._line_to_diags.values():
  175. # We also want errors to be listed before warnings so that errors aren't
  176. # hidden by the warnings; Vim won't place a sign over an existing one.
  177. diags.sort( key = lambda diag: ( diag[ 'kind' ],
  178. diag[ 'location' ][ 'column_num' ] ) )
  179. _DiagnosticIsError = CompileLevel( 'error' )
  180. _DiagnosticIsWarning = CompileLevel( 'warning' )
  181. def _NormalizeDiagnostic( diag ):
  182. def ClampToOne( value ):
  183. return value if value > 0 else 1
  184. location = diag[ 'location' ]
  185. location[ 'column_num' ] = ClampToOne( location[ 'column_num' ] )
  186. location[ 'line_num' ] = ClampToOne( location[ 'line_num' ] )
  187. return diag
  188. def _ConvertDiagnosticToTextProperties( bufnr, diagnostic ):
  189. properties = []
  190. name = ( 'YcmErrorProperty' if _DiagnosticIsError( diagnostic ) else
  191. 'YcmWarningProperty' )
  192. if vimsupport.VimIsNeovim():
  193. name = name.replace( 'Property', 'Section' )
  194. location_extent = diagnostic[ 'location_extent' ]
  195. if location_extent[ 'start' ][ 'line_num' ] <= 0:
  196. location = diagnostic[ 'location' ]
  197. line, column = vimsupport.LineAndColumnNumbersClamped(
  198. bufnr,
  199. location[ 'line_num' ],
  200. location[ 'column_num' ]
  201. )
  202. properties.append( ( line, column, name, {} ) )
  203. else:
  204. start_line, start_column = vimsupport.LineAndColumnNumbersClamped(
  205. bufnr,
  206. location_extent[ 'start' ][ 'line_num' ],
  207. location_extent[ 'start' ][ 'column_num' ]
  208. )
  209. end_line, end_column = vimsupport.LineAndColumnNumbersClamped(
  210. bufnr,
  211. location_extent[ 'end' ][ 'line_num' ],
  212. location_extent[ 'end' ][ 'column_num' ]
  213. )
  214. properties.append( (
  215. start_line,
  216. start_column,
  217. name,
  218. { 'end_lnum': end_line,
  219. 'end_col': end_column } ) )
  220. for diagnostic_range in diagnostic[ 'ranges' ]:
  221. start_line, start_column = vimsupport.LineAndColumnNumbersClamped(
  222. bufnr,
  223. diagnostic_range[ 'start' ][ 'line_num' ],
  224. diagnostic_range[ 'start' ][ 'column_num' ]
  225. )
  226. end_line, end_column = vimsupport.LineAndColumnNumbersClamped(
  227. bufnr,
  228. diagnostic_range[ 'end' ][ 'line_num' ],
  229. diagnostic_range[ 'end' ][ 'column_num' ]
  230. )
  231. if not _IsValidRange( start_line, start_column, end_line, end_column ):
  232. continue
  233. properties.append( (
  234. start_line,
  235. start_column,
  236. name,
  237. { 'end_lnum': end_line,
  238. 'end_col': end_column } ) )
  239. return properties
  240. def _IsValidRange( start_line, start_column, end_line, end_column ):
  241. # End line before start line - invalid
  242. if start_line > end_line:
  243. return False
  244. # End line after start line - valid
  245. if start_line < end_line:
  246. return True
  247. # Same line, start colum after end column - invalid
  248. if start_column > end_column:
  249. return False
  250. # Same line, start column before or equal to end column - valid
  251. return True