@@ -24,6 +24,8 @@ import com.facebook.ktfmt.kdoc.Escaping
2424import com.facebook.ktfmt.kdoc.KDocCommentsHelper
2525import com.google.common.collect.ImmutableList
2626import com.google.common.collect.Range
27+ import com.google.common.collect.RangeSet
28+ import com.google.common.collect.TreeRangeSet
2729import com.google.googlejavaformat.Doc
2830import com.google.googlejavaformat.DocBuilder
2931import 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