def _Format(self, request_data): filepath = request_data['filepath'] self._Reload(request_data) # TODO: support all formatting options. See # https://github.com/Microsoft/TypeScript/blob/72e92a055823f1ade97d03d7526dbab8be405dde/lib/protocol.d.ts#L2060-L2077 # for the list of options. While not standard, a way to support these # options, which is already adopted by a number of clients, would be to read # the "formatOptions" field in the tsconfig.json file. options = request_data['options'] self._SendRequest( 'configure', { 'file': filepath, 'formatOptions': { 'tabSize': options['tab_size'], 'indentSize': options['tab_size'], 'convertTabsToSpaces': options['insert_spaces'], } }) response = self._SendRequest('format', _BuildTsFormatRange(request_data)) contents = GetFileLines(request_data, filepath) chunks = [ _BuildFixItChunkForRange(text_edit['newText'], contents, filepath, text_edit) for text_edit in response ] location = responses.Location(request_data['line_num'], request_data['column_num'], filepath) return responses.BuildFixItResponse( [responses.FixIt(location, chunks)])
def _Format(self, request_data): request = self._DefaultParameters(request_data) request['WantsTextChanges'] = True if 'range' in request_data: lines = request_data['lines'] start = request_data['range']['start'] start_line_num = start['line_num'] start_line_value = lines[start_line_num] start_codepoint = ByteOffsetToCodepointOffset( start_line_value, start['column_num']) end = request_data['range']['end'] end_line_num = end['line_num'] end_line_value = lines[end_line_num] end_codepoint = ByteOffsetToCodepointOffset( end_line_value, end['column_num']) request.update({ 'line': start_line_num, 'column': start_codepoint, 'EndLine': end_line_num, 'EndColumn': end_codepoint }) result = self._GetResponse('/formatRange', request) else: result = self._GetResponse('/codeformat', request) fixit = responses.FixIt( _BuildLocation(request_data, request_data['filepath'], request_data['line_num'], request_data['column_codepoint']), _LinePositionSpanTextChangeToFixItChunks(result['Changes'], request_data['filepath'], request_data)) return responses.BuildFixItResponse([fixit])
def _ResolveFixIt( self, request_data, unresolved_fixit = None ): fixit = unresolved_fixit if unresolved_fixit else request_data[ 'fixit' ] if not fixit[ 'resolve' ]: return { 'fixits': [ fixit ] } fixit = fixit[ 'command' ] code_action = fixit[ 'index' ] request = self._DefaultParameters( request_data ) request.update( { 'CodeAction': code_action, 'WantsTextChanges': True, } ) response = self._GetResponse( '/runcodeaction', request ) fixit = responses.FixIt( _BuildLocation( request_data, request_data[ 'filepath' ], request_data[ 'line_num' ], request_data[ 'column_codepoint' ] ), _LinePositionSpanTextChangeToFixItChunks( response[ 'Changes' ], request_data[ 'filepath' ], request_data ), response[ 'Text' ] ) # The sort is necessary to keep the tests stable. # Python's sort() is stable, so it won't mess up the order within a file. fixit.chunks.sort( key = lambda c: c.range.start_.filename_ ) return responses.BuildFixItResponse( [ fixit ] )
def _ConvertDetailedCompletionData(request_data, completion_data): name = completion_data['name'] display_parts = completion_data['displayParts'] signature = ''.join([part['text'] for part in display_parts]) if name == signature: extra_menu_info = None detailed_info = [] else: # Strip new lines and indentation from the signature to display it on one # line. extra_menu_info = re.sub('\s+', ' ', signature) detailed_info = [signature] docs = completion_data.get('documentation', []) detailed_info += [doc['text'].strip() for doc in docs if doc] detailed_info = '\n\n'.join(detailed_info) fixits = None if 'codeActions' in completion_data: location = responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']) fixits = responses.BuildFixItResponse([ responses.FixIt( location, _BuildFixItForChanges(request_data, action['changes']), action['description']) for action in completion_data['codeActions'] ]) return responses.BuildCompletionData(insertion_text=name, extra_menu_info=extra_menu_info, detailed_info=detailed_info, kind=completion_data['kind'], extra_data=fixits)
def _RefactorRename(self, request_data, args): if len(args) != 1: raise ValueError('Please specify a new name to rename it to.\n' 'Usage: RefactorRename <new name>') self._Reload(request_data) response = self._SendRequest( 'rename', { 'file': request_data['filepath'], 'line': request_data['line_num'], 'offset': request_data['column_codepoint'], 'findInComments': False, 'findInStrings': False, }) if not response['info']['canRename']: raise RuntimeError( 'Value cannot be renamed: ' f'{ response[ "info" ][ "localizedErrorMessage" ] }') # The format of the response is: # # body { # info { # ... # triggerSpan: { # length: original_length # } # } # # locs [ { # file: file_path # locs: [ # start: { # line: line_num # offset: offset # } # end { # line: line_num # offset: offset # } # ] } # ] # } # new_name = args[0] location = responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']) chunks = [] for file_replacement in response['locs']: chunks.extend( _BuildFixItChunksForFile(request_data, new_name, file_replacement)) return responses.BuildFixItResponse( [responses.FixIt(location, chunks)])
def _TsDiagnosticToYcmdDiagnostic(self, request_data, ts_diagnostic): filepath = request_data['filepath'] ts_fixes = self._SendRequest( 'getCodeFixes', { 'file': filepath, 'startLine': ts_diagnostic['startLocation']['line'], 'startOffset': ts_diagnostic['startLocation']['offset'], 'endLine': ts_diagnostic['endLocation']['line'], 'endOffset': ts_diagnostic['endLocation']['offset'], 'errorCodes': [ts_diagnostic['code']] }) location = responses.Location(request_data['line_num'], request_data['column_num'], filepath) fixits = [] for fix in ts_fixes: description = fix['description'] # TSServer returns these fixits for every error in JavaScript files. # Ignore them since they are not useful. if description in [ 'Ignore this error message', 'Disable checking for this file' ]: continue fixit = responses.FixIt( location, _BuildFixItForChanges(request_data, fix['changes']), description) fixits.append(fixit) contents = GetFileLines(request_data, filepath) ts_start_location = ts_diagnostic['startLocation'] ts_start_line = ts_start_location['line'] start_offset = utils.CodepointOffsetToByteOffset( contents[ts_start_line - 1], ts_start_location['offset']) ts_end_location = ts_diagnostic['endLocation'] ts_end_line = ts_end_location['line'] end_offset = utils.CodepointOffsetToByteOffset( contents[ts_end_line - 1], ts_end_location['offset']) location_start = responses.Location(ts_start_line, start_offset, filepath) location_end = responses.Location(ts_end_line, end_offset, filepath) location_extent = responses.Range(location_start, location_end) return responses.Diagnostic([location_extent], location_start, location_extent, ts_diagnostic['message'], 'ERROR', fixits=fixits)
def _RefactoringToFixIt(refactoring): """Converts a Jedi Refactoring instance to a single responses.FixIt.""" # FIXME: refactorings can rename files (apparently). ycmd API doesn't have any # interface for that, so we just ignore them. changes = refactoring.get_changed_files() chunks = [] # We sort the files to ensure the tests are stable for filename in sorted(changes.keys()): changed_file = changes[filename] # NOTE: This is an internal API. We _could_ use GetFileContents( filename ) # here, but using Jedi's representation means that it is always consistent # with get_new_code() old_text = changed_file._module_node.get_code() new_text = changed_file.get_new_code() # Cache the offsets of all the newlines in the file. These are used to # calculate the line/column values from the offsets retuned by the diff # scanner newlines = [i for i, c in enumerate(old_text) if c == '\n'] newlines.append(len(old_text)) sequence_matcher = difflib.SequenceMatcher(a=old_text, b=new_text, autojunk=False) for (operation, old_start, old_end, new_start, new_end) in sequence_matcher.get_opcodes(): # Tag of equal means the range is identical, so nothing to do. if operation == 'equal': continue # operation can be 'insert', 'replace' or 'delete', the offsets actually # already cover that in our FixIt API (a delete has an empty new_text, an # insert has an empty range), so we just encode the line/column offset and # the replacement text extracted from new_text chunks.append( responses.FixItChunk( new_text[new_start:new_end], # FIXME: new_end must be equal to or after new_start, so we should make # OffsetToPosition take 2 offsets and return them rather than repeating # work responses.Range( _OffsetToPosition(old_start, filename, old_text, newlines), _OffsetToPosition(old_end, filename, old_text, newlines)))) return responses.FixIt(responses.Location(1, 1, 'none'), chunks, '', kind=responses.FixIt.Kind.REFACTOR)
def _ApplySuggestionsToFixIt(request_data, command): LOGGER.debug('command = %s', command) args = command['arguments'] text_edits = [{'range': args[0]['range'], 'newText': args[1]}] uri = args[0]['uri'] return responses.FixIt( responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']), language_server_completer.TextEditToChunks(request_data, uri, text_edits), args[1])
def _BuildCompletionFixIts(request_data, entry): if 'codeActions' in entry: location = responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']) return responses.BuildFixItResponse([ responses.FixIt( location, _BuildFixItForChanges(request_data, action['changes']), action['description']) for action in entry['codeActions'] ]) return {}
def _ModifiedFilesToFixIt(changes, request_data): chunks = [] for change in changes: chunks.extend( _LinePositionSpanTextChangeToFixItChunks(change['Changes'], change['FileName'], request_data)) # The sort is necessary to keep the tests stable. # Python's sort() is stable, so it won't mess up the order within a file. chunks.sort(key=lambda c: c.range.start_.filename_) return responses.FixIt( _BuildLocation(request_data, request_data['filepath'], request_data['line_num'], request_data['column_codepoint']), chunks)
def _FixIt(self, request_data): request = self._DefaultParameters(request_data) result = self._GetResponse('/fixcodeissue', request) replacement_text = result["Text"] location = responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']) fixits = [ responses.FixIt(location, _BuildChunks(request_data, replacement_text)) ] return responses.BuildFixItResponse(fixits)
def _FixIt( self, request_data ): request = self._DefaultParameters( request_data ) result = self._GetResponse( '/fixcodeissue', request ) replacement_text = result[ "Text" ] # Note: column_num is already a byte offset so we don't need to use # _BuildLocation. location = responses.Location( request_data[ 'line_num' ], request_data[ 'column_num' ], request_data[ 'filepath' ] ) fixits = [ responses.FixIt( location, _BuildChunks( request_data, replacement_text ) ) ] return responses.BuildFixItResponse( fixits )
def _TsDiagnosticToYcmdDiagnostic( self, request_data, ts_diagnostic ): filepath = request_data[ 'filepath' ] ts_fixes = self._SendRequest( 'getCodeFixes', { 'file': filepath, 'startLine': ts_diagnostic[ 'startLocation' ][ 'line' ], 'startOffset': ts_diagnostic[ 'startLocation' ][ 'offset' ], 'endLine': ts_diagnostic[ 'endLocation' ][ 'line' ], 'endOffset': ts_diagnostic[ 'endLocation' ][ 'offset' ], 'errorCodes': [ ts_diagnostic[ 'code' ] ] } ) location = responses.Location( request_data[ 'line_num' ], request_data[ 'column_num' ], filepath ) fixits = [ responses.FixIt( location, _BuildFixItForChanges( request_data, fix[ 'changes' ] ), fix[ 'description' ] ) for fix in ts_fixes ] contents = GetFileLines( request_data, filepath ) ts_start_location = ts_diagnostic[ 'startLocation' ] ts_start_line = ts_start_location[ 'line' ] start_offset = utils.CodepointOffsetToByteOffset( contents[ ts_start_line - 1 ], ts_start_location[ 'offset' ] ) ts_end_location = ts_diagnostic[ 'endLocation' ] ts_end_line = ts_end_location[ 'line' ] end_offset = utils.CodepointOffsetToByteOffset( contents[ ts_end_line - 1 ], ts_end_location[ 'offset' ] ) location_start = responses.Location( ts_start_line, start_offset, filepath ) location_end = responses.Location( ts_end_line, end_offset, filepath ) location_extent = responses.Range( location_start, location_end ) return responses.Diagnostic( [ location_extent ], location_start, location_extent, ts_diagnostic[ 'message' ], 'ERROR', fixits = fixits )
def _OrganizeImports(self, request_data): self._Reload(request_data) filepath = request_data['filepath'] changes = self._SendRequest( 'organizeImports', {'scope': { 'type': 'file', 'args': { 'file': filepath } }}) location = responses.Location(request_data['line_num'], request_data['column_num'], filepath) return responses.BuildFixItResponse([ responses.FixIt(location, _BuildFixItForChanges(request_data, changes)) ])
def _Rename(self, request_data, args): if len(args) != 1: raise ValueError('Please specify a new name to rename it to.\n' 'Usage: RefactorRename <new name>') query = { 'type': 'rename', 'newName': args[0], } response = self._GetResponse(query, request_data['column_codepoint'], request_data) # Tern response format: # 'changes': [ # { # 'file' (potentially relative path) # 'start' { # 'line' # 'ch' (codepoint offset) # } # 'end' { # 'line' # 'ch' (codepoint offset) # } # 'text' # } # ] # ycmd response format: # # { # 'fixits': [ # 'chunks': (list<Chunk>) [ # { # 'replacement_text', # 'range' (Range) { # 'start_' (Location): { # 'line_number_', # 'column_number_', (byte offset) # 'filename_' (note: absolute path!) # }, # 'end_' (Location): { # 'line_number_', # 'column_number_', (byte offset) # 'filename_' (note: absolute path!) # } # } # } # ], # 'location' (Location) { # 'line_number_', # 'column_number_', # 'filename_' (note: absolute path!) # } # # ] # } def BuildRange(file_contents, filename, start, end): return responses.Range( _BuildLocation(file_contents, filename, start['line'], start['ch']), _BuildLocation(file_contents, filename, end['line'], end['ch'])) def BuildFixItChunk(change): filepath = self._ServerPathToAbsolute(change['file']) file_contents = utils.SplitLines( GetFileContents(request_data, filepath)) return responses.FixItChunk( change['text'], BuildRange(file_contents, filepath, change['start'], change['end'])) # From an API perspective, Refactor and FixIt are the same thing - it just # applies a set of changes to a set of files. So we re-use all of the # existing FixIt infrastructure. return responses.BuildFixItResponse([ responses.FixIt( responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']), [BuildFixItChunk(x) for x in response['changes']]) ])
def _Rename(self, request_data, args): if len(args) != 1: raise ValueError('Please specify a new name to rename it to.\n' 'Usage: RefactorRename <new name>') query = { 'type': 'rename', 'newName': args[0], } response = self._GetResponse(query, request_data) # Tern response format: # 'changes': [ # { # 'file' # 'start' { # 'line' # 'ch' # } # 'end' { # 'line' # 'ch' # } # 'text' # } # ] # ycmd response format: # # { # 'fixits': [ # 'chunks': (list<Chunk>) [ # { # 'replacement_text', # 'range' (Range) { # 'start_' (Location): { # 'line_number_', # 'column_number_', # 'filename_' # }, # 'end_' (Location): { # 'line_number_', # 'column_number_', # 'filename_' # } # } # } # ], # 'location' (Location) { # 'line_number_', # 'column_number_', # 'filename_' # } # # ] # } def BuildRange(filename, start, end): return responses.Range( responses.Location(start['line'] + 1, start['ch'] + 1, filename), responses.Location(end['line'] + 1, end['ch'] + 1, filename)) def BuildFixItChunk(change): return responses.FixItChunk( change['text'], BuildRange(os.path.abspath(change['file']), change['start'], change['end'])) # From an API perspective, Refactor and FixIt are the same thing - it just # applies a set of changes to a set of files. So we re-use all of the # existing FixIt infrastructure. return responses.BuildFixItResponse([ responses.FixIt( responses.Location(request_data['line_num'], request_data['column_num'], request_data['filepath']), [BuildFixItChunk(x) for x in response['changes']]) ])