Skip to content

Commit 3bedf86

Browse files
committed
Wire partial formatting ranges into Formatter
1 parent bfcda5b commit 3bedf86

2 files changed

Lines changed: 153 additions & 16 deletions

File tree

core/api/ktfmt.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,11 @@ public final class com/facebook/ktfmt/format/Formatter {
116116
public static final field KOTLINLANG_FORMAT Lcom/facebook/ktfmt/format/FormattingOptions;
117117
public static final field META_FORMAT Lcom/facebook/ktfmt/format/FormattingOptions;
118118
public static final fun format (Lcom/facebook/ktfmt/format/FormattingOptions;Ljava/lang/String;)Ljava/lang/String;
119+
public static final fun format (Lcom/facebook/ktfmt/format/FormattingOptions;Ljava/lang/String;Lcom/google/common/collect/RangeSet;)Ljava/lang/String;
120+
public static final fun format (Lcom/facebook/ktfmt/format/FormattingOptions;Ljava/lang/String;Lcom/google/common/collect/RangeSet;Lcom/google/common/collect/RangeSet;)Ljava/lang/String;
119121
public static final fun format (Ljava/lang/String;)Ljava/lang/String;
120122
public static final fun format (Ljava/lang/String;Z)Ljava/lang/String;
123+
public static synthetic fun format$default (Lcom/facebook/ktfmt/format/FormattingOptions;Ljava/lang/String;Lcom/google/common/collect/RangeSet;Lcom/google/common/collect/RangeSet;ILjava/lang/Object;)Ljava/lang/String;
121124
}
122125

123126
public final class com/facebook/ktfmt/format/FormattingOptions {

core/src/main/java/com/facebook/ktfmt/format/Formatter.kt

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import com.facebook.ktfmt.kdoc.Escaping
2424
import com.facebook.ktfmt.kdoc.KDocCommentsHelper
2525
import com.google.common.collect.ImmutableList
2626
import com.google.common.collect.Range
27+
import com.google.common.collect.RangeSet
28+
import com.google.common.collect.TreeRangeSet
2729
import com.google.googlejavaformat.Doc
2830
import com.google.googlejavaformat.DocBuilder
2931
import com.google.googlejavaformat.Newlines
@@ -85,11 +87,26 @@ object Formatter {
8587
format(META_FORMAT.copy(removeUnusedImports = removeUnusedImports), code)
8688

8789
/**
88-
* format formats the Kotlin code given in 'code' with the 'maxWidth' and returns it as a string.
90+
* Formats the Kotlin code given in [code] and returns it as a string.
91+
*
92+
* @param lineRanges zero-indexed line ranges to format, using closed-open bounds, or null to
93+
* format all code
94+
* @param characterRanges zero-indexed character ranges to format, using closed-open bounds, or
95+
* null to use only [lineRanges]
96+
*
97+
* When [lineRanges] or [characterRanges] are non-null, only pretty-print replacements are limited
98+
* to those ranges. Whole-file cleanup passes, such as import cleanup and multiline string
99+
* formatting, still run afterward, mirroring google-java-format's cleanup-after-selection behavior.
89100
*/
90101
@JvmStatic
102+
@JvmOverloads
91103
@Throws(FormatterException::class, ParseError::class)
92-
fun format(options: FormattingOptions, code: String): String {
104+
fun format(
105+
options: FormattingOptions,
106+
code: String,
107+
lineRanges: RangeSet<Int>? = null,
108+
characterRanges: RangeSet<Int>? = null,
109+
): String {
93110
val (shebang, kotlinCode) =
94111
if (code.startsWith("#!")) {
95112
code.split("\n".toRegex(), limit = 2)
@@ -98,20 +115,54 @@ object Formatter {
98115
}
99116
checkEscapeSequences(kotlinCode)
100117

101-
return kotlinCode
102-
.let { convertLineSeparators(it) }
103-
.let { sortedAndDistinctImports(it) }
104-
.let { dropRedundantElements(it, options) }
105-
.let { addRedundantElements(it, options) }
106-
.let { prettyPrint(it, options, lineSeparator = "\n") }
107-
.let { addRedundantElements(it, options) }
108-
.let { MultilineStringFormatter(options.continuationIndent).format(it) }
118+
val normalizedKotlinCode = convertLineSeparators(kotlinCode)
119+
val formattedCode =
120+
if (lineRanges == null && characterRanges == null) {
121+
normalizedKotlinCode
122+
.let { sortedAndDistinctImports(it) }
123+
.let { dropRedundantElements(it, options) }
124+
.let { addRedundantElements(it, options) }
125+
.let { prettyPrint(it, options, lineSeparator = "\n") }
126+
.let { addRedundantElements(it, options) }
127+
.let { MultilineStringFormatter(options.continuationIndent).format(it) }
128+
} else {
129+
val selectedCharacterRanges =
130+
characterRangesForPartialFormatting(
131+
normalizedKotlinCode,
132+
lineRanges,
133+
characterRanges,
134+
shebang,
135+
)
136+
val partiallyFormattedCode =
137+
if (selectedCharacterRanges.isEmpty) {
138+
normalizedKotlinCode
139+
} else {
140+
prettyPrint(
141+
normalizedKotlinCode,
142+
options,
143+
lineSeparator = "\n",
144+
characterRanges = selectedCharacterRanges.asRanges(),
145+
)
146+
}
147+
partiallyFormattedCode
148+
.let { sortedAndDistinctImports(it) }
149+
.let { dropRedundantElements(it, options) }
150+
.let { addRedundantElements(it, options) }
151+
.let { MultilineStringFormatter(options.continuationIndent).format(it) }
152+
}
153+
154+
return formattedCode
109155
.let { convertLineSeparators(it, checkNotNull(Newlines.guessLineSeparator(kotlinCode))) }
110156
.let { if (shebang.isEmpty()) it else shebang + "\n" + it }
111157
}
112158

113159
/** prettyPrint reflows 'code' using google-java-format's engine. */
114-
private fun prettyPrint(code: String, options: FormattingOptions, lineSeparator: String): String {
160+
private fun prettyPrint(
161+
code: String,
162+
options: FormattingOptions,
163+
lineSeparator: String,
164+
characterRanges: Collection<Range<Int>> = ImmutableList.of(Range.closedOpen(0, code.length)),
165+
): String {
115166
val file = Parser.parse(code)
116167
val kotlinInput = KotlinInput(code, file)
117168
val javaOutput =
@@ -129,13 +180,87 @@ object Formatter {
129180
doc.write(javaOutput)
130181
javaOutput.flush()
131182

132-
val tokenRangeSet =
133-
kotlinInput.characterRangesToTokenRanges(ImmutableList.of(Range.closedOpen(0, code.length)))
183+
val tokenRangeSet = kotlinInput.characterRangesToTokenRanges(characterRanges)
134184
return WhitespaceTombstones.replaceTombstoneWithTrailingWhitespace(
135185
JavaOutput.applyReplacements(code, javaOutput.getFormatReplacements(tokenRangeSet))
136186
)
137187
}
138188

189+
/** Converts zero-indexed, closed-open line ranges to character ranges in [input]. */
190+
private fun lineRangesToCharRanges(input: String, lineRanges: RangeSet<Int>): RangeSet<Int> {
191+
val lineOffsets = mutableListOf<Int>()
192+
val lineOffsetIterator = Newlines.lineOffsetIterator(input)
193+
while (lineOffsetIterator.hasNext()) {
194+
lineOffsets.add(lineOffsetIterator.next())
195+
}
196+
lineOffsets.add(input.length + 1)
197+
198+
val characterRanges = TreeRangeSet.create<Int>()
199+
for (lineRange in
200+
lineRanges.subRangeSet(Range.closedOpen(0, lineOffsets.size - 1)).asRanges()) {
201+
val lineStart = lineOffsets[lineRange.lowerEndpoint()]
202+
val lineEnd = lineOffsets[lineRange.upperEndpoint()] - 1
203+
val characterRange = Range.closedOpen(lineStart, lineEnd)
204+
if (!characterRange.isEmpty) {
205+
characterRanges.add(characterRange)
206+
}
207+
}
208+
return characterRanges
209+
}
210+
211+
private fun characterRangesForPartialFormatting(
212+
code: String,
213+
lineRanges: RangeSet<Int>?,
214+
characterRanges: RangeSet<Int>?,
215+
shebang: String,
216+
): RangeSet<Int> {
217+
val selectedCharacterRanges = TreeRangeSet.create<Int>()
218+
if (lineRanges != null) {
219+
val adjustedLineRanges = adjustLineRangesForShebang(lineRanges, shebang.isNotEmpty())
220+
selectedCharacterRanges.addAll(lineRangesToCharRanges(code, adjustedLineRanges))
221+
}
222+
if (characterRanges != null) {
223+
selectedCharacterRanges.addAll(adjustCharacterRangesForShebang(characterRanges, shebang))
224+
}
225+
return selectedCharacterRanges
226+
}
227+
228+
private fun adjustLineRangesForShebang(
229+
lineRanges: RangeSet<Int>,
230+
hasShebang: Boolean,
231+
): RangeSet<Int> {
232+
if (!hasShebang) {
233+
return lineRanges
234+
}
235+
236+
val adjusted = TreeRangeSet.create<Int>()
237+
for (lineRange in lineRanges.subRangeSet(Range.atLeast(1)).asRanges()) {
238+
adjusted.add(Range.closedOpen(lineRange.lowerEndpoint() - 1, lineRange.upperEndpoint() - 1))
239+
}
240+
return adjusted
241+
}
242+
243+
private fun adjustCharacterRangesForShebang(
244+
characterRanges: RangeSet<Int>,
245+
shebang: String,
246+
): RangeSet<Int> {
247+
if (shebang.isEmpty()) {
248+
return characterRanges
249+
}
250+
251+
val adjusted = TreeRangeSet.create<Int>()
252+
val kotlinCodeStart = shebang.length + 1
253+
for (characterRange in characterRanges.subRangeSet(Range.atLeast(kotlinCodeStart)).asRanges()) {
254+
adjusted.add(
255+
Range.closedOpen(
256+
characterRange.lowerEndpoint() - kotlinCodeStart,
257+
characterRange.upperEndpoint() - kotlinCodeStart,
258+
)
259+
)
260+
}
261+
return adjusted
262+
}
263+
139264
private fun createAstVisitor(options: FormattingOptions, builder: OpsBuilder): PsiElementVisitor {
140265
if (KotlinVersion.CURRENT < MINIMUM_KOTLIN_VERSION) {
141266
throw RuntimeException("Unsupported runtime Kotlin version: " + KotlinVersion.CURRENT)
@@ -190,10 +315,19 @@ object Formatter {
190315
val sortedImports = importList.imports.sortedBy(::canonicalText).distinctBy(::canonicalText)
191316
val importsWithComments = commentList + sortedImports
192317

318+
val replaceStart =
319+
if (code.substring(0, importList.startOffset).isBlank()) 0 else importList.startOffset
320+
var replaceEnd = importList.endOffset
321+
while (replaceEnd < code.length && code[replaceEnd].isWhitespace()) {
322+
replaceEnd++
323+
}
324+
val trailingNewlines = if (replaceEnd < code.length) "\n\n" else "\n"
325+
193326
return code.replaceRange(
194-
importList.startOffset,
195-
importList.endOffset,
196-
importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text } + "\n",
327+
replaceStart,
328+
replaceEnd,
329+
importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text } +
330+
trailingNewlines,
197331
)
198332
}
199333
}

0 commit comments

Comments
 (0)