11import json
22import logging
33import os
4- from collections .abc import Iterator , Sequence
4+ from collections .abc import Iterable , Iterator , Reversible , Sequence
55from contextlib import contextmanager
66from dataclasses import asdict , dataclass , field
77from difflib import SequenceMatcher
@@ -256,6 +256,13 @@ def kind(self) -> str:
256256 def symbol_kind (self ) -> SymbolKind :
257257 return self .symbol_root ["kind" ]
258258
259+ def is_neighbouring_definition_separated_by_empty_line (self ) -> bool :
260+ """
261+ :return: whether a symbol definition of this symbol's kind is usually separated from the
262+ previous/next definition by at least one empty line.
263+ """
264+ return self .symbol_kind in (SymbolKind .Function , SymbolKind .Method , SymbolKind .Class , SymbolKind .Interface , SymbolKind .Struct )
265+
259266 @property
260267 def relative_path (self ) -> str | None :
261268 location = self .symbol_root .get ("location" )
@@ -689,26 +696,36 @@ def replace_body_at_location(self, location: SymbolLocation, body: str, *, use_s
689696 if start_pos is None or end_pos is None :
690697 raise ValueError (f"Symbol at { location } does not have a defined body range." )
691698 start_line , start_col = start_pos ["line" ], start_pos ["character" ]
699+
692700 if use_same_indentation :
693701 indent = " " * start_col
694702 body_lines = body .splitlines ()
695703 body = body_lines [0 ] + "\n " + "\n " .join (indent + line for line in body_lines [1 :])
696704
697- # make sure body always ends with at least one newline
698- if not body .endswith ("\n " ):
699- body += "\n "
705+ # make sure the replacement adds no additional newlines (before or after) - all newlines
706+ # and whitespace before/after should remain the same, so we strip it entirely
707+ body = body .strip ()
708+
700709 self ._lang_server .delete_text_between_positions (location .relative_path , start_pos , end_pos )
701710 self ._lang_server .insert_text_at_position (location .relative_path , start_line , start_col , body )
702711
703- def insert_after_symbol (
704- self ,
705- name_path : str ,
706- relative_file_path : str ,
707- body : str ,
708- * ,
709- use_same_indentation : bool = True ,
710- at_new_line : bool = True ,
711- ) -> None :
712+ @staticmethod
713+ def _count_leading_newlines (text : Iterable ) -> int :
714+ cnt = 0
715+ for c in text :
716+ if c == "\n " :
717+ cnt += 1
718+ elif c == "\r " :
719+ continue
720+ else :
721+ break
722+ return cnt
723+
724+ @classmethod
725+ def _count_trailing_newlines (cls , text : Reversible ) -> int :
726+ return cls ._count_leading_newlines (reversed (text ))
727+
728+ def insert_after_symbol (self , name_path : str , relative_file_path : str , body : str , * , use_same_indentation : bool = True ) -> None :
712729 """
713730 Inserts content after the symbol with the given name in the given file.
714731 """
@@ -722,13 +739,9 @@ def insert_after_symbol(
722739 f"Found symbols at locations: \n " + json .dumps ([s .location .to_dict () for s in symbol_candidates ], indent = 2 )
723740 )
724741 symbol = symbol_candidates [- 1 ]
725- return self .insert_after_symbol_at_location (
726- symbol .location , body , at_new_line = at_new_line , use_same_indentation = use_same_indentation
727- )
742+ return self .insert_after_symbol_at_location (symbol .location , body , use_same_indentation = use_same_indentation )
728743
729- def insert_after_symbol_at_location (
730- self , location : SymbolLocation , body : str , * , at_new_line : bool = True , use_same_indentation : bool = True
731- ) -> None :
744+ def insert_after_symbol_at_location (self , location : SymbolLocation , body : str , * , use_same_indentation : bool = True ) -> None :
732745 """
733746 Appends content after the given symbol
734747
@@ -750,12 +763,23 @@ def insert_after_symbol_at_location(
750763 if pos is None :
751764 raise ValueError (f"Symbol at { location } does not have a defined end position." )
752765
753- line , col = pos ["line" ], pos ["character" ]
754- if at_new_line :
755- line += 1
756- col = 0
757- if not body .startswith ("\n " ):
758- body = "\n " + body
766+ # start at the beginning of the next line
767+ col = 0
768+ line = pos ["line" ] + 1
769+ # make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type,
770+ # otherwise as many as the caller wanted to insert)
771+ original_leading_newlines = self ._count_leading_newlines (body )
772+ body = body .lstrip ("\r \n " )
773+ min_empty_lines = 0
774+ if symbol .is_neighbouring_definition_separated_by_empty_line ():
775+ min_empty_lines = 1
776+ num_leading_empty_lines = max (min_empty_lines , original_leading_newlines )
777+ if num_leading_empty_lines :
778+ body = ("\n " * num_leading_empty_lines ) + body
779+ # make sure the one line break succeeding the original symbol, which we repurposed as prefix via
780+ # `line += 1`, is replaced
781+ body = body .rstrip ("\r \n " ) + "\n "
782+
759783 if use_same_indentation :
760784 symbol_start_pos = symbol .body_start_position
761785 assert symbol_start_pos is not None , f"Symbol at { location = } does not have a defined start position."
@@ -778,20 +802,11 @@ def insert_after_symbol_at_location(
778802 # > test test
779803 # > second line
780804 # > dataclass_instance.status = "active" # Reassign dataclass field
781- col = 0
782805
783806 with self ._edited_symbol_location (location ):
784807 self ._lang_server .insert_text_at_position (location .relative_path , line = line , column = col , text_to_be_inserted = body )
785808
786- def insert_before_symbol (
787- self ,
788- name_path : str ,
789- relative_file_path : str ,
790- body : str ,
791- * ,
792- at_new_line : bool = True ,
793- use_same_indentation : bool = True ,
794- ) -> None :
809+ def insert_before_symbol (self , name_path : str , relative_file_path : str , body : str , * , use_same_indentation : bool = True ) -> None :
795810 """
796811 Inserts content before the symbol with the given name in the given file.
797812 """
@@ -805,11 +820,9 @@ def insert_before_symbol(
805820 f"Found symbols at locations: \n " + json .dumps ([s .location .to_dict () for s in symbol_candidates ], indent = 2 )
806821 )
807822 symbol = symbol_candidates [0 ]
808- self .insert_before_symbol_at_location (symbol .location , body , at_new_line = at_new_line , use_same_indentation = use_same_indentation )
823+ self .insert_before_symbol_at_location (symbol .location , body , use_same_indentation = use_same_indentation )
809824
810- def insert_before_symbol_at_location (
811- self , location : SymbolLocation , body : str , * , at_new_line : bool = True , use_same_indentation : bool = True
812- ) -> None :
825+ def insert_before_symbol_at_location (self , location : SymbolLocation , body : str , * , use_same_indentation : bool = True ) -> None :
813826 """
814827 Inserts content before the given symbol
815828
@@ -820,18 +833,28 @@ def insert_before_symbol_at_location(
820833 symbol_start_pos = symbol .body_start_position
821834 if symbol_start_pos is None :
822835 raise ValueError (f"Symbol at { location } does not have a defined start position." )
823- line = symbol_start_pos ["line" ]
824- col = symbol_start_pos ["character" ]
836+
825837 if use_same_indentation :
826- indent = " " * (col )
838+ indent = " " * (symbol_start_pos [ "character" ] )
827839 body = "\n " .join (indent + line for line in body .splitlines ())
828840
829- # similar problems as in insert_after_symbol_at_location, see comment there
830- if at_new_line :
831- col = 0
832- line -= 1
833- if not body .endswith ("\n " ):
834- body += "\n "
841+ # insert position is the start of line where the symbol is defined
842+ line = symbol_start_pos ["line" ]
843+ col = 0
844+
845+ original_trailing_empty_lines = self ._count_trailing_newlines (body ) - 1
846+
847+ # ensure eol is present at end
848+ body = body .rstrip () + "\n "
849+
850+ # add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type,
851+ # otherwise as many as the caller wanted to insert)
852+ min_trailing_empty_lines = 0
853+ if symbol .is_neighbouring_definition_separated_by_empty_line ():
854+ min_trailing_empty_lines = 1
855+ num_trailing_newlines = max (min_trailing_empty_lines , original_trailing_empty_lines )
856+ body += "\n " * num_trailing_newlines
857+
835858 assert location .relative_path is not None
836859
837860 self ._lang_server .insert_text_at_position (location .relative_path , line = line , column = col , text_to_be_inserted = body )
0 commit comments