Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions grimoire.scrbl
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ programmatically on anything found here.
@include-section[(lib "resyntax/grimoire/source.scrbl")]
@include-section[(lib "resyntax/grimoire/source-group.scrbl")]
@include-section[(lib "resyntax/grimoire/syntax-path.scrbl")]
@include-section[(lib "resyntax/grimoire/string-replacement.scrbl")]
45 changes: 31 additions & 14 deletions private/string-replacement.rkt → grimoire/string-replacement.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
#:chaperone
(#:start [start natural?]
#:end [end natural?]
#:contents [contents (sequence/c (or/c inserted-string? copied-string?))])
#:contents [contents (sequence/c string-piece?)])
#:pre/name (start end) "end cannot be before start" (<= start end)
[_ string-replacement?])]
[replacement-string-span (-> (or/c inserted-string? copied-string?) exact-nonnegative-integer?)]
[string-piece? (-> any/c boolean?)]
[string-piece-span (-> string-piece? exact-nonnegative-integer?)]
[string-replacement-start (-> string-replacement? natural?)]
[string-replacement-original-end (-> string-replacement? natural?)]
[string-replacement-original-span (-> string-replacement? natural?)]
[string-replacement-new-end (-> string-replacement? natural?)]
[string-replacement-new-span (-> string-replacement? natural?)]
[string-replacement-contents
(-> string-replacement? (listof (or/c inserted-string? copied-string?)))]
[string-replacement-contents (-> string-replacement? (listof string-piece?))]
[string-replacement-preserved-locations (-> string-replacement? range-set?)]
[string-replacement-overlaps? (-> string-replacement? string-replacement? boolean?)]
[string-replacement-normalize
Expand Down Expand Up @@ -74,7 +74,6 @@
rebellion/streaming/reducer
rebellion/streaming/transducer
rebellion/type/record
rebellion/type/tuple
resyntax/grimoire/source)


Expand All @@ -98,7 +97,7 @@
#:result
(reverse (append (if previous (list previous) (list)) accumulated)))
([piece contents]
#:when (positive? (replacement-string-span piece)))
#:when (positive? (string-piece-span piece)))
(match (list previous piece)
[(list #false _) (values accumulated piece)]
[(list (inserted-string s1) (inserted-string s2))
Expand All @@ -107,7 +106,7 @@
#:when (equal? end1 start2)
(values accumulated (copied-string start1 end2))]
[(list _ _) (values (cons previous accumulated) piece)])))
(define new-span (transduce content-list (mapping replacement-string-span) #:into into-sum))
(define new-span (transduce content-list (mapping string-piece-span) #:into into-sum))
(define max-end
(transduce content-list
(filtering copied-string?)
Expand All @@ -119,7 +118,7 @@
#:original-span (- end start)
#:new-end (+ start new-span)
#:new-span new-span
#:required-length (option-get max-end end)
#:required-length (max end (option-get max-end 0))
#:contents content-list))


Expand Down Expand Up @@ -207,15 +206,19 @@
(string-replacement-range replacement) (string-replacement-range other-replacement)))


(struct inserted-string (contents) #:transparent
(struct string-piece () #:transparent)


(struct inserted-string string-piece (contents) #:transparent
#:guard (λ (contents _) (string->immutable-string contents))
#:property prop:custom-print-quotable 'never)


(define-tuple-type copied-string (start end))
(struct copied-string string-piece (start end) #:transparent
#:property prop:custom-print-quotable 'never)


(define (replacement-string-span piece)
(define (string-piece-span piece)
(match piece
[(inserted-string inserted-string) (string-length inserted-string)]
[(copied-string start end) (- end start)]))
Expand All @@ -242,7 +245,7 @@
(define original-length (string-length immutable-original-string))
(unless (>= original-length required-length)
(raise-arguments-error
(name string-apply-replacement)
(name string-replacement-render)
"string is not long enough"
"string" immutable-original-string
"string length" original-length
Expand Down Expand Up @@ -320,7 +323,21 @@
(string-replacement #:start 0
#:end (string-length s)
#:contents (list (inserted-string "hi there"))))
(check-equal? (string-apply-replacement s replacement) "hi there"))))
(check-equal? (string-apply-replacement s replacement) "hi there"))

(test-case "string shorter than the replaced region"
(define replacement
(string-replacement #:start 0 #:end 10 #:contents (list (copied-string 0 2))))
(check-exn #rx"string-apply-replacement:" (λ () (string-apply-replacement "abc" replacement)))
(check-exn #rx"string is not long enough"
(λ () (string-apply-replacement "abc" replacement)))))

(test-case (name-string string-replacement-render)
(test-case "errors are reported under the right name"
(define replacement
(string-replacement #:start 0 #:end 2 #:contents (list (copied-string 10 20))))
(check-exn #rx"string-replacement-render:"
(λ () (string-replacement-render replacement "ab"))))))


(define (file-apply-string-replacement! path replacement)
Expand Down Expand Up @@ -358,7 +375,7 @@
(guarded-block
(guard (< pos actual-start) #:else pieces)
(guard-match (cons next-piece remaining) pieces #:else (list))
(define piece-span (replacement-string-span next-piece))
(define piece-span (string-piece-span next-piece))
(if (< (+ pos piece-span) actual-start)
(loop remaining (+ pos piece-span))
(cons (replacement-string-drop-left next-piece (- actual-start pos)) remaining)))))
Expand Down
205 changes: 205 additions & 0 deletions grimoire/string-replacement.scrbl
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#lang scribble/manual


@(require (for-label racket/base
racket/contract/base
racket/math
racket/sequence
rebellion/base/immutable-string
rebellion/collection/range-set
rebellion/streaming/reducer
rebellion/streaming/transducer
resyntax/grimoire/string-replacement))


@title[#:tag "string-replacement"]{String Replacements}
@defmodule[resyntax/grimoire/string-replacement]

A @deftech{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 @tech{string pieces}. There are two kinds
of pieces:

@itemlist[
@item{@emph{Inserted strings}, constructed with @racket[inserted-string], containing brand new text
to insert.}

@item{@emph{Copied strings}, constructed with @racket[copied-string], containing a range of
positions to copy from the original string. Copied pieces may copy from @emph{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 @racket[_start] to position @racket[_end] covers the characters at positions
@racket[_start] through @racket[(sub1 _end)].

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 @secref["original-syntax-paths"]: 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 @racket[copied-string] piece.


@defproc[(string-replacement? [v any/c]) boolean?]{
A predicate that recognizes @tech{string replacements}.}


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

The contents are 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 @racket[equal?], and
@racket[string-replacement-contents] may return a different list than the one the replacement was
constructed with.}


@defproc[(string-replacement-start [replacement string-replacement?]) natural?]{
Returns the position of the first character replaced by @racket[replacement].}


@defproc[(string-replacement-original-end [replacement string-replacement?]) natural?]{
Returns the position just past the last character replaced by @racket[replacement], in terms of
positions within the @emph{original} string.}


@defproc[(string-replacement-original-span [replacement string-replacement?]) natural?]{
Returns the number of characters of the original string that @racket[replacement] replaces.}


@defproc[(string-replacement-new-end [replacement string-replacement?]) natural?]{
Returns the position just past the replaced region within the @emph{edited} string produced by
applying @racket[replacement], equal to the replacement's start position plus its new span.}


@defproc[(string-replacement-new-span [replacement string-replacement?]) natural?]{
Returns the total number of characters that @racket[replacement]'s contents span, which is the
length of the text that the replaced region contains after the replacement is applied.}


@defproc[(string-replacement-contents [replacement string-replacement?])
(listof string-piece?)]{
Returns the @tech{string pieces} making up the new contents of @racket[replacement]'s replaced
region, in normalized form.}


@defproc[(string-replacement-preserved-locations [replacement string-replacement?]) range-set?]{
Returns a range set of the positions in the original string whose characters are preserved by
@racket[replacement]: every position before the replaced region, every position after it, and every
position within the source range of one of the replacement's @racket[copied-string] pieces.}


@defproc[(string-replacement-overlaps? [replacement string-replacement?]
[other-replacement string-replacement?])
boolean?]{
Returns @racket[#true] if the replaced regions of @racket[replacement] and
@racket[other-replacement] overlap. Replacements whose regions merely touch at a boundary do not
overlap, since regions are half-open.}


@defproc[(string-replacement-union [replacement1 string-replacement?]
[replacement2 string-replacement?])
string-replacement?]{
Combines @racket[replacement1] and @racket[replacement2] into a single @tech{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 @racket[string-replacement-overlaps?]).}


@defthing[union-into-string-replacement (reducer/c string-replacement? string-replacement?)]{
A @tech[#:doc '(lib "rebellion/main.scrbl")]{reducer} that combines a sequence of pairwise
non-overlapping @tech{string replacements} into one, as with @racket[string-replacement-union], for
use with @racket[transduce]. The reduction starts from an empty replacement at position @racket[0],
so the combined replacement's region always starts at position @racket[0].}


@defproc[(string-replacement-normalize
[replacement string-replacement?]
[original-string string?]
[#:preserve-start preserve-start (or/c exact-nonnegative-integer? #false) #false]
[#:preserve-end preserve-end (or/c exact-nonnegative-integer? #false) #false])
string-replacement?]{
Returns a @tech{string replacement} equivalent to @racket[replacement] --- applying it to
@racket[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 @racket[preserve-start] is provided, the normalized region is never trimmed
past it: the region always starts at or before @racket[preserve-start]. Likewise, if
@racket[preserve-end] is provided, the normalized region always ends at or after
@racket[preserve-end]. Replacements that don't change @racket[original-string] at all are returned
unchanged.}


@defproc[(string-replacement-render [replacement string-replacement?] [original-string string?])
immutable-string?]{
Returns the new text of @racket[replacement]'s replaced region --- just the rendered contents, not
the entire edited string. The original string is needed to render the contents of
@racket[copied-string] pieces. Raises a contract error if @racket[original-string] is too short to
contain the replaced region or the positions that the replacement's copied pieces refer to.}


@defproc[(string-apply-replacement [string string?] [replacement string-replacement?])
immutable-string?]{
Applies @racket[replacement] to @racket[string], returning the entire edited string. Text outside
the replaced region is unchanged. Raises a contract error if @racket[string] is too short to
contain the replaced region or the positions that the replacement's copied pieces refer to.}


@defproc[(file-apply-string-replacement! [path path-string?] [replacement string-replacement?])
void?]{
Reads the file at @racket[path], applies @racket[replacement] to its contents as with
@racket[string-apply-replacement], and overwrites the file with the result.}


@section{String Pieces}

A @deftech{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 @tech{string replacement}'s replaced
region, but they are also occasionally useful on their own as standalone descriptions of text.


@defproc[(string-piece? [v any/c]) boolean?]{
A predicate that recognizes @tech{string pieces} of either kind.}


@defproc[(inserted-string? [v any/c]) boolean?]{
A predicate that recognizes inserted-string @tech{string pieces}. Implies @racket[string-piece?].}


@defproc[(inserted-string [contents string?]) inserted-string?]{
Constructs a @tech{string piece} containing @racket[contents] as brand new text.}


@defproc[(inserted-string-contents [piece inserted-string?]) immutable-string?]{
Returns the text that @racket[piece] inserts.}


@defproc[(copied-string? [v any/c]) boolean?]{
A predicate that recognizes copied-string @tech{string pieces}. Implies @racket[string-piece?].}


@defproc[(copied-string [start natural?] [end natural?]) copied-string?]{
Constructs a @tech{string piece} that copies the characters between @racket[start] and
@racket[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 @racket[end] is
before @racket[start].}


@defproc[(copied-string-start [piece copied-string?]) natural?]{
Returns the position of the first character that @racket[piece] copies.}


@defproc[(copied-string-end [piece copied-string?]) natural?]{
Returns the position just past the last character that @racket[piece] copies.}


@defproc[(string-piece-span [piece string-piece?]) exact-nonnegative-integer?]{
Returns the number of characters that @racket[piece] spans: the length of an inserted string's
text, or the size of a copied string's range.}
2 changes: 1 addition & 1 deletion main.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
resyntax/private/refactoring-result
resyntax/grimoire/source
resyntax/private/string-indent
resyntax/private/string-replacement
resyntax/grimoire/string-replacement
resyntax/private/syntax-property-bundle
resyntax/private/syntax-range
resyntax/private/syntax-replacement
Expand Down
2 changes: 1 addition & 1 deletion private/line-replacement.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
rebellion/streaming/transducer
rebellion/type/record
resyntax/private/linemap
resyntax/private/string-replacement)
resyntax/grimoire/string-replacement)


(module+ test
Expand Down
2 changes: 1 addition & 1 deletion private/refactoring-result.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
resyntax/private/linemap
resyntax/private/logger
resyntax/grimoire/source
resyntax/private/string-replacement
resyntax/grimoire/string-replacement
resyntax/private/syntax-replacement
(only-in racket/list first))

Expand Down
6 changes: 3 additions & 3 deletions private/syntax-replacement.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
resyntax/private/logger
resyntax/grimoire/source
resyntax/private/string-indent
resyntax/private/string-replacement
resyntax/grimoire/string-replacement
resyntax/private/syntax-neighbors
(only-in resyntax/private/syntax-traversal syntax-search-everything)
syntax/parse
Expand Down Expand Up @@ -200,12 +200,12 @@
(+ start
(for/sum ([piece (in-list contents-with-possible-focus)]
#:break (focus? piece))
(replacement-string-span piece))))
(string-piece-span piece))))
(define focused-end
(- end
(for/sum ([piece (in-list (reverse contents-with-possible-focus))]
#:break (focus? piece))
(replacement-string-span piece))))
(string-piece-span piece))))
(define raw-contents
(append-map (λ (piece) (if (focus? piece) (focus-contents piece) (list piece)))
contents-with-possible-focus))
Expand Down
2 changes: 1 addition & 1 deletion test/private/rackunit.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
resyntax/private/refactoring-result
resyntax/grimoire/source
resyntax/private/string-indent
resyntax/private/string-replacement
resyntax/grimoire/string-replacement
resyntax/grimoire/syntax-path
resyntax/private/syntax-property-bundle
resyntax/private/syntax-traversal
Expand Down
Loading