def _BuildChunks(request_data, new_buffer): filepath = request_data['filepath'] old_buffer = request_data['file_data'][filepath]['contents'] new_buffer = _FixLineEndings(old_buffer, new_buffer) new_length = len(new_buffer) old_length = len(old_buffer) if new_length == old_length and new_buffer == old_buffer: return [] min_length = min(new_length, old_length) start_index = 0 end_index = min_length for i in range(0, min_length - 1): if new_buffer[i] != old_buffer[i]: start_index = i break for i in range(1, min_length): if new_buffer[new_length - i] != old_buffer[old_length - i]: end_index = i - 1 break # To handle duplicates, i.e aba => a if (start_index + end_index > min_length): start_index -= start_index + end_index - min_length replacement_text = new_buffer[start_index:new_length - end_index] (start_line, start_column) = _IndexToLineColumn(old_buffer, start_index) (end_line, end_column) = _IndexToLineColumn(old_buffer, old_length - end_index) start = responses.Location(start_line, start_column, filepath) end = responses.Location(end_line, end_column, filepath) return [ responses.FixItChunk(replacement_text, responses.Range(start, end)) ]
def _BuildLocation(split_lines, filename, line_num, column_num): if (line_num <= 0 or column_num <= 0 or line_num - 1 >= len(split_lines)): return responses.Location(line_num, 0, filename) line_value = split_lines[line_num - 1] return responses.Location( line_num, utils.CodepointOffsetToByteOffset(line_value, column_num), 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 _BuildFixItChunkForRange(new_name, file_name, source_range): """ returns list FixItChunk for a tsserver source range """ return responses.FixItChunk( new_name, responses.Range(start=responses.Location( source_range['start']['line'], source_range['start']['offset'], file_name), end=responses.Location(source_range['end']['line'], source_range['end']['offset'], file_name)))
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 _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 _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 _BuildLocation(file_contents, filename, line, ch): # tern returns codepoint offsets, but we need byte offsets, so we must # convert return responses.Location(line=line + 1, column=utils.CodepointOffsetToByteOffset( file_contents[line], ch + 1), filename=os.path.realpath(filename))
def _BuildLocation( request_data, filename, line_num, column_num ): contents = utils.SplitLines( GetFileContents( request_data, filename ) ) line_value = contents[ line_num - 1 ] return responses.Location( line_num, CodepointOffsetToByteOffset( line_value, column_num ), filename )
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 _BuildLocation( file_contents, filename, line, offset ): return responses.Location( line = line, # tsserver returns codepoint offsets, but we need byte offsets, so we must # convert column = utils.CodepointOffsetToByteOffset( file_contents[ line - 1 ], offset ), filename = filename )
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 TsDiagnosticToYcmdDiagnostic(filepath, line_value, ts_diagnostic): ts_start_location = ts_diagnostic['startLocation'] ts_end_location = ts_diagnostic['endLocation'] start_offset = utils.CodepointOffsetToByteOffset( line_value, ts_start_location['offset']) end_offset = utils.CodepointOffsetToByteOffset(line_value, ts_end_location['offset']) location = responses.Location(ts_start_location['line'], start_offset, filepath) location_end = responses.Location(ts_end_location['line'], end_offset, filepath) location_extent = responses.Range(location, location_end) return responses.Diagnostic([location_extent], location, location_extent, ts_diagnostic['message'], 'ERROR')
def _QuickFixToDiagnostic(self, quick_fix): filename = quick_fix["FileName"] location = responses.Location(quick_fix["Line"], quick_fix["Column"], filename) location_range = responses.Range(location, location) return responses.Diagnostic(list(), location, location_range, quick_fix["Text"], quick_fix["LogLevel"].upper())
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 _BuildLocation(request_data, filename, line_num, column_num): if line_num <= 0: return None # OmniSharp sometimes incorrectly returns 0 for the column number. Assume the # column is 1 in that case. if column_num <= 0: column_num = 1 contents = GetFileLines(request_data, filename) line_value = contents[line_num - 1] return responses.Location( line_num, CodepointOffsetToByteOffset(line_value, column_num), filename)
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 TsDiagnosticToYcmdDiagnostic(request_data, ts_diagnostic): filepath = request_data['filepath'] contents = request_data['lines'] 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')
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 _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 _OffsetToPosition(offset, filename, text, newlines): """Convert the 0-based codepoint offset |offset| to a position (line/col) in |text|. |filename| is the full path of the file containing |text| and |newlines| is a cache of the 0-based character offsets of all the \n characters in |text| (plus one extra). Returns responses.Position.""" for index, newline in enumerate(newlines): if newline >= offset: start_of_line = newlines[index - 1] + 1 if index > 0 else 0 column = offset - start_of_line line_value = text[start_of_line:newline] return responses.Location( index + 1, CodepointOffsetToByteOffset(line_value, column + 1), filename) # Invalid position - it's outside of the text. Just return the last # position in the text. This is an internal error. LOGGER.error("Invalid offset %s in file %s with text %s and newlines %s", offset, filename, text, newlines) raise RuntimeError("Invalid file offset in diff")
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']]) ])