1# Copyright (C) 2013-2020 ycmd contributors 2# 3# This file is part of ycmd. 4# 5# ycmd 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# ycmd 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 ycmd. If not, see <http://www.gnu.org/licenses/>. 17 18import os 19from ycmd.utils import ProcessIsRunning 20 21 22YCM_EXTRA_CONF_FILENAME = '.ycm_extra_conf.py' 23 24CONFIRM_CONF_FILE_MESSAGE = ( 'Found {0}. Load? \n\n(Question can be turned ' 25 'off with options, see YCM docs)' ) 26 27NO_EXTRA_CONF_FILENAME_MESSAGE = ( f'No { YCM_EXTRA_CONF_FILENAME } file ' 28 'detected, so no compile flags are available. Thus no semantic support for ' 29 'C/C++/ObjC/ObjC++. Go READ THE ' 'DOCS *NOW*, DON\'T file a bug report.' ) 30 31NO_DIAGNOSTIC_SUPPORT_MESSAGE = ( 'YCM has no diagnostics support for this ' 32 'filetype; refer to Syntastic docs if using Syntastic.' ) 33 34EMPTY_SIGNATURE_INFO = { 35 'activeSignature': 0, 36 'activeParameter': 0, 37 'signatures': [], 38} 39 40 41class SignatureHelpAvailalability: 42 AVAILABLE = 'YES' 43 NOT_AVAILABLE = 'NO' 44 PENDING = 'PENDING' 45 46 47class ServerError( Exception ): 48 def __init__( self, message ): 49 super().__init__( message ) 50 51 52class UnknownExtraConf( ServerError ): 53 def __init__( self, extra_conf_file ): 54 message = CONFIRM_CONF_FILE_MESSAGE.format( extra_conf_file ) 55 super().__init__( message ) 56 self.extra_conf_file = extra_conf_file 57 58 59class NoExtraConfDetected( ServerError ): 60 def __init__( self ): 61 super().__init__( NO_EXTRA_CONF_FILENAME_MESSAGE ) 62 63 64class NoDiagnosticSupport( ServerError ): 65 def __init__( self ): 66 super().__init__( NO_DIAGNOSTIC_SUPPORT_MESSAGE ) 67 68 69# column_num is a byte offset 70def BuildGoToResponse( filepath, line_num, column_num, description = None ): 71 return BuildGoToResponseFromLocation( 72 Location( line = line_num, 73 column = column_num, 74 filename = filepath ), 75 description ) 76 77 78def BuildGoToResponseFromLocation( location, description = None ): 79 """Build a GoTo response from a responses.Location object.""" 80 response = BuildLocationData( location ) 81 if description: 82 response[ 'description' ] = description 83 return response 84 85 86def BuildDescriptionOnlyGoToResponse( text ): 87 return { 88 'description': text, 89 } 90 91 92def BuildDisplayMessageResponse( text ): 93 return { 94 'message': text 95 } 96 97 98def BuildDetailedInfoResponse( text ): 99 """ Returns the response object for displaying detailed information about types 100 and usage, such as within a preview window""" 101 return { 102 'detailed_info': text 103 } 104 105 106def BuildCompletionData( insertion_text, 107 extra_menu_info = None, 108 detailed_info = None, 109 menu_text = None, 110 kind = None, 111 extra_data = None ): 112 completion_data = { 113 'insertion_text': insertion_text 114 } 115 116 if extra_menu_info: 117 completion_data[ 'extra_menu_info' ] = extra_menu_info 118 if menu_text: 119 completion_data[ 'menu_text' ] = menu_text 120 if detailed_info: 121 completion_data[ 'detailed_info' ] = detailed_info 122 if kind: 123 completion_data[ 'kind' ] = kind 124 if extra_data: 125 completion_data[ 'extra_data' ] = extra_data 126 return completion_data 127 128 129# start_column is a byte offset 130def BuildCompletionResponse( completions, 131 start_column, 132 errors=None ): 133 return { 134 'completions': completions, 135 'completion_start_column': start_column, 136 'errors': errors if errors else [], 137 } 138 139 140def BuildResolveCompletionResponse( completion, errors ): 141 return { 142 'completion': completion, 143 'errors': errors if errors else [], 144 } 145 146 147def BuildSignatureHelpResponse( signature_info, errors = None ): 148 return { 149 'signature_help': 150 signature_info if signature_info else EMPTY_SIGNATURE_INFO, 151 'errors': errors if errors else [], 152 } 153 154 155# location.column_number_ is a byte offset 156def BuildLocationData( location ): 157 return { 158 'line_num': location.line_number_, 159 'column_num': location.column_number_, 160 'filepath': ( os.path.normpath( location.filename_ ) 161 if location.filename_ else '' ), 162 } 163 164 165def BuildRangeData( source_range ): 166 return { 167 'start': BuildLocationData( source_range.start_ ), 168 'end': BuildLocationData( source_range.end_ ), 169 } 170 171 172class Diagnostic: 173 def __init__( self, 174 ranges, 175 location, 176 location_extent, 177 text, 178 kind, 179 fixits = [] ): 180 self.ranges_ = ranges 181 self.location_ = location 182 self.location_extent_ = location_extent 183 self.text_ = text 184 self.kind_ = kind 185 self.fixits_ = fixits 186 187 188class UnresolvedFixIt: 189 def __init__( self, command, text, kind = None ): 190 self.command = command 191 self.text = text 192 self.resolve = True 193 self.kind = kind 194 195 196class FixIt: 197 """A set of replacements (of type FixItChunk) to be applied to fix a single 198 diagnostic. This can be used for any type of refactoring command, not just 199 quick fixes. The individual chunks may span multiple files. 200 201 NOTE: All offsets supplied in both |location| and (the members of) |chunks| 202 must be byte offsets into the UTF-8 encoded version of the appropriate 203 buffer. 204 """ 205 class Kind: 206 """These are LSP kinds that we use outside of LSP completers.""" 207 REFACTOR = 'refactor' 208 209 210 def __init__( self, location, chunks, text = '', kind = None ): 211 """location of type Location, chunks of type list<FixItChunk>""" 212 self.location = location 213 self.chunks = chunks 214 self.text = text 215 self.kind = kind 216 217 218class FixItChunk: 219 """An individual replacement within a FixIt (aka Refactor)""" 220 221 def __init__( self, replacement_text, range ): 222 """replacement_text of type string, range of type Range""" 223 self.replacement_text = replacement_text 224 self.range = range 225 226 227class Range: 228 """Source code range relating to a diagnostic or FixIt (aka Refactor).""" 229 230 def __init__( self, start, end ): 231 "start of type Location, end of type Location""" 232 self.start_ = start 233 self.end_ = end 234 235 236class Location: 237 """Source code location for a diagnostic or FixIt (aka Refactor).""" 238 239 def __init__( self, line, column, filename ): 240 """Line is 1-based line, column is 1-based column byte offset, filename is 241 absolute path of the file""" 242 self.line_number_ = line 243 self.column_number_ = column 244 if filename: 245 self.filename_ = os.path.abspath( filename ) 246 else: 247 # When the filename passed (e.g. by a server) can't be recognized or 248 # parsed, we send an empty filename. This at least allows the client to 249 # know there _is_ a reference, but not exactly where it is. This can 250 # happen with the Java completer which sometimes returns references using 251 # a custom/undocumented URI scheme. Typically, such URIs point to .class 252 # files or other binary data which clients can't display anyway. 253 # FIXME: Sending a location with an empty filename could be considered a 254 # strict breach of our own protocol. Perhaps completers should be required 255 # to simply skip such a location. 256 self.filename_ = filename 257 258 259def BuildDiagnosticData( diagnostic ): 260 kind = ( diagnostic.kind_.name if hasattr( diagnostic.kind_, 'name' ) 261 else diagnostic.kind_ ) 262 263 return { 264 'ranges': [ BuildRangeData( x ) for x in diagnostic.ranges_ ], 265 'location': BuildLocationData( diagnostic.location_ ), 266 'location_extent': BuildRangeData( diagnostic.location_extent_ ), 267 'text': diagnostic.text_, 268 'kind': kind, 269 'fixit_available': len( diagnostic.fixits_ ) > 0, 270 } 271 272 273def BuildDiagnosticResponse( diagnostics, 274 filename, 275 max_diagnostics_to_display ): 276 if ( max_diagnostics_to_display and 277 len( diagnostics ) > max_diagnostics_to_display ): 278 diagnostics = diagnostics[ : max_diagnostics_to_display ] 279 location = Location( 1, 1, filename ) 280 location_extent = Range( location, location ) 281 diagnostics.append( Diagnostic( 282 [ location_extent ], 283 location, 284 location_extent, 285 'Maximum number of diagnostics exceeded.', 286 'ERROR' 287 ) ) 288 return [ BuildDiagnosticData( diagnostic ) for diagnostic in diagnostics ] 289 290 291def BuildFixItResponse( fixits ): 292 """Build a response from a list of FixIt (aka Refactor) objects. This response 293 can be used to apply arbitrary changes to arbitrary files and is suitable for 294 both quick fix and refactor operations""" 295 296 def BuildFixitChunkData( chunk ): 297 return { 298 'replacement_text': chunk.replacement_text, 299 'range': BuildRangeData( chunk.range ), 300 } 301 302 def BuildFixItData( fixit ): 303 if hasattr( fixit, 'resolve' ): 304 result = { 305 'command': fixit.command, 306 'text': fixit.text, 307 'kind': fixit.kind, 308 'resolve': fixit.resolve 309 } 310 else: 311 result = { 312 'location': BuildLocationData( fixit.location ), 313 'chunks' : [ BuildFixitChunkData( x ) for x in fixit.chunks ], 314 'text': fixit.text, 315 'kind': fixit.kind, 316 'resolve': False 317 } 318 319 if result[ 'kind' ] is None: 320 result.pop( 'kind' ) 321 322 return result 323 324 return { 325 'fixits' : [ BuildFixItData( x ) for x in fixits ] 326 } 327 328 329def BuildExceptionResponse( exception, traceback ): 330 return { 331 'exception': exception, 332 'message': str( exception ), 333 'traceback': traceback 334 } 335 336 337class DebugInfoServer: 338 """Store debugging information on a server: 339 - name: the server name; 340 - is_running: True if the server process is alive, False otherwise; 341 - executable: path of the executable used to start the server; 342 - address: if applicable, the address on which the server is listening. None 343 otherwise; 344 - port: if applicable, the port on which the server is listening. None 345 otherwise; 346 - pid: the process identifier of the server. None if the server is not 347 running; 348 - logfiles: a list of logging files used by the server; 349 - extras: a list of DebugInfoItem objects for additional information on the 350 server.""" 351 352 def __init__( self, 353 name, 354 handle, 355 executable, 356 address = None, 357 port = None, 358 logfiles = [], 359 extras = [] ): 360 self.name = name 361 self.is_running = ProcessIsRunning( handle ) 362 self.executable = executable 363 self.address = address 364 self.port = port 365 self.pid = handle.pid if self.is_running else None 366 # Remove undefined logfiles from the list. 367 self.logfiles = [ logfile for logfile in logfiles if logfile ] 368 self.extras = extras 369 370 371class DebugInfoItem: 372 373 def __init__( self, key, value ): 374 self.key = key 375 self.value = value 376 377 378def BuildDebugInfoResponse( name, servers = [], items = [] ): 379 """Build a response containing debugging information on a semantic completer: 380 - name: the completer name; 381 - servers: a list of DebugInfoServer objects representing the servers used by 382 the completer; 383 - items: a list of DebugInfoItem objects for additional information 384 on the completer.""" 385 386 def BuildItemData( item ): 387 return { 388 'key': item.key, 389 'value': item.value 390 } 391 392 393 def BuildServerData( server ): 394 return { 395 'name': server.name, 396 'is_running': server.is_running, 397 'executable': server.executable, 398 'address': server.address, 399 'port': server.port, 400 'pid': server.pid, 401 'logfiles': server.logfiles, 402 'extras': [ BuildItemData( item ) for item in server.extras ] 403 } 404 405 406 return { 407 'name': name, 408 'servers': [ BuildServerData( server ) for server in servers ], 409 'items': [ BuildItemData( item ) for item in items ] 410 } 411 412 413def BuildSignatureHelpAvailableResponse( value ): 414 return { 'available': value } 415