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