On this page:
string-replacement?
string-replacement
string-replacement-start
string-replacement-original-end
string-replacement-original-span
string-replacement-new-end
string-replacement-new-span
string-replacement-contents
string-replacement-preserved-locations
string-replacement-overlaps?
string-replacement-union
union-into-string-replacement
string-replacement-focus
string-replacement-new-text
string-replacement-apply
string-replacement-apply-to-file!
4.6.1 String Pieces
string-piece?
inserted-string?
inserted-string
inserted-string-contents
copied-string?
copied-string
copied-string-start
copied-string-end
string-piece-span
9.2

4.6 String Replacements🔗

 (require resyntax/grimoire/string-replacement)
  package: resyntax

A string replacement is a value describing an edit to a string. A replacement identifies a region of the string to replace — the characters between a start position and an end position — and describes the new contents of that region as a list of string pieces. There are two kinds of pieces:

  • Inserted strings, constructed with inserted-string, containing brand new text to insert.

  • Copied strings, constructed with copied-string, containing a range of positions to copy from the original string. Copied pieces may copy from anywhere in the original string, not just from within the replaced region, so a replacement can move text around in addition to inserting and deleting it.

All positions are zero-based character offsets, and regions are half-open: a replacement from position start to position end covers the characters at positions start through (sub1 end). Beware that string replacements do NOT use one-based position indices, unlike positions in syntax object source locations such as those returned by syntax-position.

When applied to a string, a string replacement first deletes everything in the range it describes. Then it inserts and copies text into that range, according to the string replacement’s piece list. The replacement is applied atomically: character positions in copied strings always refer to the positions of characters in the original, unedited string, prior to any deletions, insertions, or copying operations being applied.

String replacements are the final, lowest-level form that Resyntax’s refactoring suggestions take before being written to files. The distinction between inserted and copied pieces is the string-level end of the formatting preservation mechanism described in Original Syntax Paths and Formatting Preservation: when Resyntax decides that a piece of refactored code is unchanged from the original program, the suggestion copies its text rather than regenerating it, and that decision ultimately takes the form of a copied-string piece. This mechanism is also used by Resyntax to preserve comments where possible and discard suggestions that can’t preserve them; see string-replacement-preserved-locations for details.

procedure

(string-replacement? v)  boolean?

  v : any/c
A predicate that recognizes string replacements.

procedure

(string-replacement #:start start    
  #:end end    
  #:contents contents)  string-replacement?
  start : natural?
  end : natural?
  contents : (sequence/c string-piece?)
Constructs a string replacement that replaces the characters between start and end with the given contents. Raises a contract error if end is before start.

The contents are lightly normalized during construction: pieces that span zero characters are dropped, adjacent inserted strings are merged into one, and adjacent copied strings that copy contiguous regions are merged into one. As a consequence, two replacements constructed from differently divided piece lists describing the same content are equal?, and string-replacement-contents may return a different list than the one the replacement was constructed with.

Beware that this normalization does not remove redundant no-op copied-string pieces at the start or end of the replacement. That is the job of the stricter replacement focusing operation implemented by string-replacement-focus. For example, consider a replacement that starts at position 0, ends at position n, and contains a single (copied-string 0 n) piece. This replacement is functionally a no-op that makes no changes at all to the string. Construction does not remove the copied-string piece, even though it behaves identically to an empty string replacement. Focusing the replacement, on the other hand, does remove the copied string piece — see string-replacement-focus for details.

procedure

(string-replacement-start replacement)  natural?

  replacement : string-replacement?
Returns the position of the first character replaced by replacement.

procedure

(string-replacement-original-end replacement)  natural?

  replacement : string-replacement?
Returns the position just past the last character replaced by replacement, in terms of positions within the original string. Note that this is a zero-indexed position, with zero being before the first character.

procedure

(string-replacement-original-span replacement)  natural?

  replacement : string-replacement?
Returns the size in characters of the range of the original string that replacement replaces. This is always equal to string-replacement-start subtracted from string-replacement-original-end.

procedure

(string-replacement-new-end replacement)  natural?

  replacement : string-replacement?
Returns the position just past the replaced region within the edited string produced by applying replacement, equal to the replacement’s start position plus its new span.

procedure

(string-replacement-new-span replacement)  natural?

  replacement : string-replacement?
Returns the total number of characters that replacement’s contents span, which is the length of the text that the replaced region contains after the replacement is applied. This is always equal to string-replacement-start subtracted from string-replacement-new-end.

procedure

(string-replacement-contents replacement)

  (listof string-piece?)
  replacement : string-replacement?
Returns the string pieces making up the new contents of replacement’s replaced region, in normalized form.

procedure

(string-replacement-preserved-locations replacement)

  range-set?
  replacement : string-replacement?
Returns a range set of the positions in the original string whose characters are preserved by replacement: every position before the replaced region, every position after it, and every position within the source range of one of the replacement’s copied-string pieces.

This is used by Resyntax to determine whether or not a replacement preserves comments: if a replacement’s unpreserved locations have any overlap with source-comment-locations for the source being edited, then the replacement drops comments and is discarded by Resyntax.

procedure

(string-replacement-overlaps? replacement    
  other-replacement)  boolean?
  replacement : string-replacement?
  other-replacement : string-replacement?
Returns #true if the replaced regions of replacement and other-replacement overlap. Replacements whose regions merely touch at a boundary do not overlap, since regions are half-open character ranges.

Overlap detection is conservatively imperfect. There exist string replacements that can be applied together safely, but which string-replacement-overlaps? reports #true for. However, there are never cases where string-replacement-overlaps? issues a false negative — only false positives. Further improvement here would likely require adjusting the representation of string replacements using a range map structure instead of a single range and a list of pieces.

procedure

(string-replacement-union replacement1    
  replacement2)  string-replacement?
  replacement1 : string-replacement?
  replacement2 : string-replacement?
Combines replacement1 and replacement2 into a single string replacement whose replaced region covers both of their regions. The text between the two regions is copied from the original string unchanged. The order of the arguments doesn’t matter. Raises a contract error if the two replacements overlap (in the sense of string-replacement-overlaps?).

A quirk of the current implementation is that empty replacements are not treated specially. The union of an empty replacement at position 0 and a replacement at position n will produce an expanded replacement that starts at position 0. A no-op copied-string piece that copies the contents between 0 and n will be included. This results in a replacement that behaves identically, but which reports an earlier start position and larger spans from string-replacement-original-span and string-replacement-new-span. Like the deficiencies of string-replacement-overlaps?, this could be fixed by using a range map representation for string replacements instead of a single range representation.

Note that even for non-overlapping replacements, applying the union of two string replacements is not the same as applying one replacement and then applying the other. This is because of copied strings — the first replacement may change the size of the string, which can cause the character positions referenced by copied string pieces in the second replacement to refer to different text than it would have if the replacements were applied in the opposite order. This is because although individual string replacements can be applied atomically, multiple replacements cannot.

When Resyntax wants to apply multiple string replacements at once, it always combines their replacements into a single union replacement. Conflicting replacements are dropped; rather than try to apply them in a second stage, Resyntax throws them out and re-analyzes the entire modified source file instead to generate fresh suggested replacements. As a result, Resyntax has to decide which replacements it wants to apply in each analysis round before it can begin the next analysis round. The result of each analysis round edits the source code in-memory using modified-source so that Resyntax can interleave editing and analysis in this manner without actually committing its edits to the filesystem.

A reducer that combines a sequence of pairwise non-overlapping string replacements into one, as with string-replacement-union, for use with transduce. The reduction starts from an empty replacement at position 0, so the combined replacement’s region always starts at position 0.

Note that because of the quirk with how string-replacement-union handles empty replacements, this implies that a replacement produced by union-into-string-replacement always starts at position 0 and includes a large no-op copied-string piece between position 0 and the lowest-position replacement that was included in the union.

procedure

(string-replacement-focus replacement 
  original-string 
  [#:preserve-start preserve-start 
  #:preserve-end preserve-end]) 
  string-replacement?
  replacement : string-replacement?
  original-string : string?
  preserve-start : (or/c exact-nonnegative-integer? #false)
   = #false
  preserve-end : (or/c exact-nonnegative-integer? #false)
   = #false
Returns a string replacement equivalent to replacement applying it to original-string produces the same result — but whose replaced region is as small as possible. Leading and trailing portions of the replacement that leave the original text unchanged are trimmed away. If preserve-start is provided, the focused region is never trimmed past it: the region always starts at or before preserve-start. Likewise, if preserve-end is provided, the focused region always ends at or after preserve-end. Replacements that don’t change original-string at all are shrunk to the smallest no-op replacements possible given the constraints of preserve-start and preserve-end, and shrunk to empty replacements starting at their original start if those constraints are not provided.

This operation is called replacement focusing, and is more aggressive than the normalization performed automatically by the string-replacement constructor. This is the low-level mechanism used by Resyntax to implement the replacement focusing behavior described in Narrowing the Focus of Replacements.

procedure

(string-replacement-new-text replacement 
  original-string) 
  immutable-string?
  replacement : string-replacement?
  original-string : string?
Returns the new text of replacement’s replaced region — just the rendered contents, not the entire edited string. The length of the returned string is always equal to string-replacement-new-span, and it’s the text that occupies the region ending at string-replacement-new-end once the replacement is applied. The original string is needed to render the contents of copied-string pieces. Raises a contract error if original-string is too short to contain the replaced region or the positions that the replacement’s copied pieces refer to.

procedure

(string-replacement-apply replacement    
  string)  immutable-string?
  replacement : string-replacement?
  string : string?
Applies replacement to string, returning the entire edited string. Text outside the replaced region is unchanged. Raises a contract error if string is too short to contain the replaced region or the positions that the replacement’s copied pieces refer to.

procedure

(string-replacement-apply-to-file! replacement    
  path)  void?
  replacement : string-replacement?
  path : path-string?
Reads the file at path, applies replacement to its contents as with string-replacement-apply, and overwrites the file with the result.

4.6.1 String Pieces🔗

A string piece describes a segment of text, either brand new or copied from some original string. String pieces primarily serve as the contents of a string replacement’s replaced region, but they are also occasionally useful on their own as standalone descriptions of text.

procedure

(string-piece? v)  boolean?

  v : any/c
A predicate that recognizes string pieces of either kind.

procedure

(inserted-string? v)  boolean?

  v : any/c
A predicate that recognizes inserted-string string pieces. Implies string-piece?.

procedure

(inserted-string contents)  inserted-string?

  contents : string?
Constructs a string piece containing contents as brand new text.

procedure

(inserted-string-contents piece)  immutable-string?

  piece : inserted-string?
Returns the text that piece inserts.

procedure

(copied-string? v)  boolean?

  v : any/c
A predicate that recognizes copied-string string pieces. Implies string-piece?.

procedure

(copied-string start end)  copied-string?

  start : natural?
  end : natural?
Constructs a string piece that copies the characters between start and end from the original string. The copied range may lie anywhere within the original string, including entirely outside the replaced region. Raises a contract error if end is before start.

procedure

(copied-string-start piece)  natural?

  piece : copied-string?
Returns the position of the first character that piece copies.

procedure

(copied-string-end piece)  natural?

  piece : copied-string?
Returns the position just past the last character that piece copies.

procedure

(string-piece-span piece)  exact-nonnegative-integer?

  piece : string-piece?
Returns the number of characters that piece spans: the length of an inserted string’s text, or the size of a copied string’s range.