Skip to content

Commit 70a74dc

Browse files
authored
Merge pull request #1640 from mnikulin/master
Add more Refactor* subcommands for python
2 parents 323d4b6 + 7e67216 commit 70a74dc

File tree

2 files changed

+330
-25
lines changed

2 files changed

+330
-25
lines changed

ycmd/completers/python/python_completer.py

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ def GetSubcommandsMap( self ):
293293
self._GetDoc( request_data ) ),
294294
'RefactorRename' : ( lambda self, request_data, args:
295295
self._RefactorRename( request_data, args ) ),
296+
'RefactorInline' : ( lambda self, request_data, args:
297+
self._RefactorInline( request_data, args ) ),
298+
'RefactorExtractVariable' : ( lambda self, request_data, args:
299+
self._RefactorExtractVariable( request_data,
300+
args ) ),
301+
'RefactorExtractFunction' : ( lambda self, request_data, args:
302+
self._RefactorExtractFunction( request_data,
303+
args ) ),
296304
}
297305

298306

@@ -464,15 +472,71 @@ def _RefactorRename( self, request_data, args ):
464472
_RefactoringToFixIt( refactoring )
465473
] )
466474

475+
def _RefactorInline( self, request_data, args ):
476+
with self._jedi_lock:
477+
refactoring = self._GetJediScript( request_data ).inline(
478+
line = request_data[ 'line_num' ],
479+
column = request_data[ 'column_codepoint' ] - 1 )
480+
481+
return responses.BuildFixItResponse( [
482+
_RefactoringToFixIt( refactoring )
483+
] )
484+
485+
def _RefactorExtractVariable( self, request_data, args ):
486+
if len( args ) < 1:
487+
raise RuntimeError( 'Must specify a new name' )
488+
489+
new_name = args[ 0 ]
490+
if 'range' in request_data:
491+
range_end = request_data[ 'range' ].get( 'end', {} )
492+
until_line = range_end.get( 'line_num', None )
493+
until_column = range_end.get( 'column_num', None )
494+
else:
495+
until_line = None
496+
until_column = None
497+
498+
with self._jedi_lock:
499+
refactoring = self._GetJediScript( request_data ).extract_variable(
500+
line = request_data[ 'line_num' ],
501+
column = request_data[ 'column_codepoint' ] - 1,
502+
new_name = new_name,
503+
until_line = until_line,
504+
until_column = until_column )
505+
506+
return responses.BuildFixItResponse( [
507+
_RefactoringToFixIt( refactoring )
508+
] )
509+
510+
def _RefactorExtractFunction( self, request_data, args ):
511+
if len( args ) < 1:
512+
raise RuntimeError( 'Must specify a new name' )
513+
514+
new_name = args[ 0 ]
515+
if 'range' in request_data:
516+
range_end = request_data[ 'range' ].get( 'end', {} )
517+
until_line = range_end.get( 'line_num', None )
518+
until_column = range_end.get( 'column_num', None )
519+
else:
520+
until_line = None
521+
until_column = None
522+
523+
with self._jedi_lock:
524+
refactoring = self._GetJediScript( request_data ).extract_function(
525+
line = request_data[ 'line_num' ],
526+
column = request_data[ 'column_codepoint' ] - 1,
527+
new_name = new_name,
528+
until_line = until_line,
529+
until_column = until_column )
530+
531+
return responses.BuildFixItResponse( [
532+
_RefactoringToFixIt( refactoring )
533+
] )
534+
467535
# Jedi has the following refactorings:
468-
# - renmae (RefactorRename)
536+
# - rename (RefactorRename)
469537
# - inline variable
470538
# - extract variable (requires argument)
471539
# - extract function (requires argument)
472-
#
473-
# We could add inline variable via FixIt, but for the others we have no way to
474-
# ask for the argument on "resolve" of the FixIt. We could add
475-
# Refactor Inline ... but that would be inconsistent.
476540

477541

478542
def DebugInfo( self, request_data ):
@@ -554,17 +618,10 @@ def _RefactoringToFixIt( refactoring ):
554618
# the replacement text extracted from new_text
555619
chunks.append( responses.FixItChunk(
556620
new_text[ new_start : new_end ],
557-
# FIXME: new_end must be equal to or after new_start, so we should make
558-
# OffsetToPosition take 2 offsets and return them rather than repeating
559-
# work
560-
responses.Range( _OffsetToPosition( old_start,
561-
filename,
562-
old_text,
563-
newlines ),
564-
_OffsetToPosition( old_end,
565-
filename,
566-
old_text,
567-
newlines ) )
621+
responses.Range( *_OffsetToPosition( ( old_start, old_end ),
622+
filename,
623+
old_text,
624+
newlines ) )
568625
) )
569626

570627
return responses.FixIt( responses.Location( 1, 1, 'none' ),
@@ -573,21 +630,26 @@ def _RefactoringToFixIt( refactoring ):
573630
kind = responses.FixIt.Kind.REFACTOR )
574631

575632

576-
def _OffsetToPosition( offset, filename, text, newlines ):
633+
def _OffsetToPosition( start_end, filename, text, newlines ):
577634
"""Convert the 0-based codepoint offset |offset| to a position (line/col) in
578635
|text|. |filename| is the full path of the file containing |text| and
579636
|newlines| is a cache of the 0-based character offsets of all the \n
580637
characters in |text| (plus one extra). Returns responses.Position."""
581638

639+
loc = ()
582640
for index, newline in enumerate( newlines ):
583-
if newline >= offset:
584-
start_of_line = newlines[ index - 1 ] + 1 if index > 0 else 0
585-
column = offset - start_of_line
586-
line_value = text[ start_of_line : newline ]
587-
return responses.Location( index + 1,
588-
CodepointOffsetToByteOffset( line_value,
589-
column + 1 ),
590-
filename )
641+
for offset in start_end[ len( loc ): ]:
642+
if newline >= offset:
643+
start_of_line = newlines[ index - 1 ] + 1 if index > 0 else 0
644+
column = offset - start_of_line
645+
line_value = text[ start_of_line : newline ]
646+
loc += ( responses.Location( index + 1,
647+
CodepointOffsetToByteOffset( line_value,
648+
column + 1 ),
649+
filename ), )
650+
if len( loc ) == 2:
651+
break
652+
return loc
591653

592654
# Invalid position - it's outside of the text. Just return the last
593655
# position in the text. This is an internal error.

ycmd/tests/python/subcommands_test.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,3 +781,246 @@ def test_Subcommands_RefactorRename_Module( self, app ):
781781
} )
782782
)
783783
} ) )
784+
785+
786+
@SharedYcmd
787+
def test_Subcommands_RefactorInline( self, app ):
788+
one = PathToTestFile( 'rename', 'one.py' )
789+
contents = ReadFile( one )
790+
791+
command_data = BuildRequest( filepath = one,
792+
filetype = 'python',
793+
line_num = 8,
794+
column_num = 10,
795+
contents = contents,
796+
command_arguments = [ 'RefactorInline' ] )
797+
798+
response = app.post_json( '/run_completer_command',
799+
command_data ).json
800+
801+
assert_that( response, has_entries( {
802+
'fixits': contains_exactly(
803+
has_entries( {
804+
'text': '',
805+
'chunks': contains_exactly(
806+
ChunkMatcher( '',
807+
LocationMatcher( one, 8, 10 ),
808+
LocationMatcher( one, 9, 10 ) ),
809+
ChunkMatcher( '',
810+
LocationMatcher( one, 9, 61 ),
811+
LocationMatcher( one, 9, 66 ) ),
812+
ChunkMatcher( ' + MODULE',
813+
LocationMatcher( one, 9, 74 ),
814+
LocationMatcher( one, 9, 74 ) ),
815+
ChunkMatcher( 'SCOPE',
816+
LocationMatcher( one, 9, 75 ),
817+
LocationMatcher( one, 9, 75 ) )
818+
)
819+
} )
820+
)
821+
} ) )
822+
823+
824+
@SharedYcmd
825+
def test_Subcommands_RefactorExtractVariable_NoNewName( self, app ):
826+
filepath = PathToTestFile( 'basic.py' )
827+
contents = ReadFile( filepath )
828+
command_data = BuildRequest( filepath = filepath,
829+
filetype = 'python',
830+
line_num = 3,
831+
column_num = 10,
832+
contents = contents,
833+
command_arguments = [
834+
'RefactorExtractVariable'
835+
] )
836+
837+
response = app.post_json( '/run_completer_command',
838+
command_data,
839+
expect_errors = True )
840+
841+
assert_that( response.status_code,
842+
equal_to( requests.codes.internal_server_error ) )
843+
assert_that( response.json,
844+
ErrorMatcher( RuntimeError, 'Must specify a new name' ) )
845+
846+
847+
@SharedYcmd
848+
def test_Subcommands_RefactorExtractVariable_Same( self, app ):
849+
filepath = PathToTestFile( 'basic.py' )
850+
contents = ReadFile( filepath )
851+
852+
command_data = BuildRequest( filepath = filepath,
853+
filetype = 'python',
854+
line_num = 3,
855+
column_num = 14,
856+
contents = contents,
857+
command_arguments = [
858+
'RefactorExtractVariable',
859+
'c'
860+
] )
861+
862+
response = app.post_json( '/run_completer_command',
863+
command_data ).json
864+
865+
assert_that( response, has_entries( {
866+
'fixits': contains_exactly(
867+
has_entries( {
868+
'text': '',
869+
'chunks': contains_exactly(
870+
ChunkMatcher( 'c = 1\n ',
871+
LocationMatcher( filepath, 3, 5 ),
872+
LocationMatcher( filepath, 3, 5 ) ),
873+
ChunkMatcher( 'c',
874+
LocationMatcher( filepath, 3, 14 ),
875+
LocationMatcher( filepath, 3, 15 ) )
876+
)
877+
} )
878+
)
879+
} ) )
880+
881+
882+
@SharedYcmd
883+
def test_Subcommands_RefactorExtractVariable_Until( self, app ):
884+
filepath = PathToTestFile( 'signature_help.py' )
885+
contents = ReadFile( filepath )
886+
887+
command_data = BuildRequest( filepath = filepath,
888+
filetype = 'python',
889+
line_num = 14,
890+
column_num = 24,
891+
range = {
892+
'end': {
893+
'line_num': 14,
894+
'column_num': 36
895+
}
896+
},
897+
contents = contents,
898+
command_arguments = [
899+
'RefactorExtractVariable',
900+
'c'
901+
] )
902+
903+
response = app.post_json( '/run_completer_command',
904+
command_data ).json
905+
906+
assert_that( response, has_entries( {
907+
'fixits': contains_exactly(
908+
has_entries( {
909+
'text': '',
910+
'chunks': contains_exactly(
911+
ChunkMatcher( "c = 'test'.center\n ",
912+
LocationMatcher( filepath, 14, 5 ),
913+
LocationMatcher( filepath, 14, 5 ) ),
914+
ChunkMatcher( '',
915+
LocationMatcher( filepath, 14, 24 ),
916+
LocationMatcher( filepath, 14, 31 ) ),
917+
ChunkMatcher( '',
918+
LocationMatcher( filepath, 14, 32 ),
919+
LocationMatcher( filepath, 14, 37 ) ),
920+
)
921+
} )
922+
)
923+
} ) )
924+
@SharedYcmd
925+
def test_Subcommands_RefactorExtractFunction_NoNewName( self, app ):
926+
filepath = PathToTestFile( 'basic.py' )
927+
contents = ReadFile( filepath )
928+
command_data = BuildRequest( filepath = filepath,
929+
filetype = 'python',
930+
line_num = 3,
931+
column_num = 10,
932+
contents = contents,
933+
command_arguments = [
934+
'RefactorExtractFunction',
935+
] )
936+
937+
response = app.post_json( '/run_completer_command',
938+
command_data,
939+
expect_errors = True )
940+
941+
assert_that( response.status_code,
942+
equal_to( requests.codes.internal_server_error ) )
943+
assert_that( response.json,
944+
ErrorMatcher( RuntimeError, 'Must specify a new name' ) )
945+
946+
947+
@SharedYcmd
948+
def test_Subcommands_RefactorExtractFunction_Same( self, app ):
949+
filepath = PathToTestFile( 'basic.py' )
950+
contents = ReadFile( filepath )
951+
952+
command_data = BuildRequest( filepath = filepath,
953+
filetype = 'python',
954+
line_num = 3,
955+
column_num = 14,
956+
contents = contents,
957+
command_arguments = [
958+
'RefactorExtractFunction',
959+
'c'
960+
] )
961+
962+
response = app.post_json( '/run_completer_command',
963+
command_data ).json
964+
965+
assert_that( response, has_entries( {
966+
'fixits': contains_exactly(
967+
has_entries( {
968+
'text': '',
969+
'chunks': contains_exactly(
970+
ChunkMatcher( '\n def c(self):\n return 1\n',
971+
LocationMatcher( filepath, 1, 19 ),
972+
LocationMatcher( filepath, 1, 19 ) ),
973+
ChunkMatcher( 'self.c()',
974+
LocationMatcher( filepath, 3, 14 ),
975+
LocationMatcher( filepath, 3, 15 ) )
976+
)
977+
} )
978+
)
979+
} ) )
980+
981+
982+
@SharedYcmd
983+
def test_Subcommands_RefactorExtractFunction_Until( self, app ):
984+
filepath = PathToTestFile( 'signature_help.py' )
985+
contents = ReadFile( filepath )
986+
987+
command_data = BuildRequest( filepath = filepath,
988+
filetype = 'python',
989+
line_num = 14,
990+
column_num = 24,
991+
range = {
992+
'end': {
993+
'line_num': 14,
994+
'column_num': 36
995+
}
996+
},
997+
contents = contents,
998+
command_arguments = [
999+
'RefactorExtractFunction',
1000+
'c'
1001+
] )
1002+
1003+
response = app.post_json( '/run_completer_command',
1004+
command_data ).json
1005+
1006+
assert_that( response, has_entries( {
1007+
'fixits': contains_exactly(
1008+
has_entries( {
1009+
'text': '',
1010+
'chunks': contains_exactly(
1011+
ChunkMatcher( "\n def c(self):\n return 'test'.center\n",
1012+
LocationMatcher( filepath, 9, 13 ),
1013+
LocationMatcher( filepath, 9, 13 ) ),
1014+
ChunkMatcher( 's',
1015+
LocationMatcher( filepath, 14, 24 ),
1016+
LocationMatcher( filepath, 14, 26 ) ),
1017+
ChunkMatcher( 'lf',
1018+
LocationMatcher( filepath, 14, 27 ),
1019+
LocationMatcher( filepath, 14, 30 ) ),
1020+
ChunkMatcher( '()',
1021+
LocationMatcher( filepath, 14, 32 ),
1022+
LocationMatcher( filepath, 14, 37 ) ),
1023+
)
1024+
} )
1025+
)
1026+
} ) )

0 commit comments

Comments
 (0)