• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) Tor Norbye.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.facebook.ktfmt.kdoc
18 
19 import java.util.regex.Pattern
20 import kotlin.math.min
21 
getIndentnull22 fun getIndent(width: Int): String {
23   val sb = StringBuilder()
24   for (i in 0 until width) {
25     sb.append(' ')
26   }
27   return sb.toString()
28 }
29 
getIndentSizenull30 fun getIndentSize(indent: String, options: KDocFormattingOptions): Int {
31   var size = 0
32   for (c in indent) {
33     if (c == '\t') {
34       size += options.tabWidth
35     } else {
36       size++
37     }
38   }
39   return size
40 }
41 
42 /** Returns line number (1-based) */
getLineNumbernull43 fun getLineNumber(source: String, offset: Int, startLine: Int = 1, startOffset: Int = 0): Int {
44   var line = startLine
45   for (i in startOffset until offset) {
46     val c = source[i]
47     if (c == '\n') {
48       line++
49     }
50   }
51   return line
52 }
53 
54 private val numberPattern = Pattern.compile("^\\d+([.)]) ")
55 
Stringnull56 fun String.isListItem(): Boolean {
57   return startsWith("- ") ||
58       startsWith("* ") ||
59       startsWith("+ ") ||
60       firstOrNull()?.isDigit() == true && numberPattern.matcher(this).find() ||
61       startsWith("<li>", ignoreCase = true)
62 }
63 
collapseSpacesnull64 fun String.collapseSpaces(): String {
65   if (indexOf("  ") == -1) {
66     return this.trimEnd()
67   }
68   val sb = StringBuilder()
69   var prev: Char = this[0]
70   for (i in indices) {
71     if (prev == ' ') {
72       if (this[i] == ' ') {
73         continue
74       }
75     }
76     sb.append(this[i])
77     prev = this[i]
78   }
79   return sb.trimEnd().toString()
80 }
81 
isTodonull82 fun String.isTodo(): Boolean {
83   return startsWith("TODO:") || startsWith("TODO(")
84 }
85 
isHeadernull86 fun String.isHeader(): Boolean {
87   return startsWith("#") || startsWith("<h", true)
88 }
89 
isQuotednull90 fun String.isQuoted(): Boolean {
91   return startsWith("> ")
92 }
93 
isDirectiveMarkernull94 fun String.isDirectiveMarker(): Boolean {
95   return startsWith("<!--") || startsWith("-->")
96 }
97 
98 /**
99  * Returns true if the string ends with a symbol that implies more text is coming, e.g. ":" or ","
100  */
isExpectingMorenull101 fun String.isExpectingMore(): Boolean {
102   val last = lastOrNull { !it.isWhitespace() } ?: return false
103   return last == ':' || last == ','
104 }
105 
106 /**
107  * Does this String represent a divider line? (Markdown also requires it to be surrounded by empty
108  * lines which has to be checked by the caller)
109  */
Stringnull110 fun String.isLine(minCount: Int = 3): Boolean {
111   return startsWith('-') && containsOnly('-', ' ') && count { it == '-' } >= minCount ||
112       startsWith('_') && containsOnly('_', ' ') && count { it == '_' } >= minCount
113 }
114 
isKDocTagnull115 fun String.isKDocTag(): Boolean {
116   // Not using a hardcoded list here since tags can change over time
117   if (startsWith("@") && length > 1) {
118     for (i in 1 until length) {
119       val c = this[i]
120       if (c.isWhitespace()) {
121         return i > 2
122       } else if (!c.isLetter() || !c.isLowerCase()) {
123         if (c == '[' && (startsWith("@param") || startsWith("@property"))) {
124           // @param is allowed to use brackets -- see
125           // https://kotlinlang.org/docs/kotlin-doc.html#param-name
126           // Example: @param[foo] The description of foo
127           return true
128         } else if (i == 1 && c.isLetter() && c.isUpperCase()) {
129           // Allow capitalized tgs, such as @See -- this is normally a typo; convertMarkup
130           // should also fix these.
131           return true
132         }
133         return false
134       }
135     }
136     return true
137   }
138   return false
139 }
140 
141 /**
142  * If this String represents a KDoc `@param` tag, returns the corresponding parameter name,
143  * otherwise null.
144  */
getParamNamenull145 fun String.getParamName(): String? {
146   val length = this.length
147   var start = 0
148   while (start < length && this[start].isWhitespace()) {
149     start++
150   }
151   if (!this.startsWith("@param", start)) {
152     return null
153   }
154   start += "@param".length
155 
156   while (start < length) {
157     if (this[start].isWhitespace()) {
158       start++
159     } else {
160       break
161     }
162   }
163 
164   if (start < length && this[start] == '[') {
165     start++
166     while (start < length) {
167       if (this[start].isWhitespace()) {
168         start++
169       } else {
170         break
171       }
172     }
173   }
174 
175   var end = start
176   while (end < length) {
177     if (!this[end].isJavaIdentifierPart()) {
178       break
179     }
180     end++
181   }
182 
183   if (end > start) {
184     return this.substring(start, end)
185   }
186 
187   return null
188 }
189 
getIndentnull190 private fun getIndent(start: Int, lookup: (Int) -> Char): String {
191   var i = start - 1
192   while (i >= 0 && lookup(i) != '\n') {
193     i--
194   }
195   val sb = StringBuilder()
196   for (j in i + 1 until start) {
197     sb.append(lookup(j))
198   }
199   return sb.toString()
200 }
201 
202 /**
203  * Given a character [lookup] function in a document of [max] characters, for a comment starting at
204  * offset [start], compute the effective indent on the first line and on subsequent lines.
205  *
206  * For a comment starting on its own line, the two will be the same. But for a comment that is at
207  * the end of a line containing code, the first line indent will not be the indentation of the
208  * earlier code, it will be the full indent as if all the code characters were whitespace characters
209  * (which lets the formatter figure out how much space is available on the first line).
210  */
computeIndentsnull211 fun computeIndents(start: Int, lookup: (Int) -> Char, max: Int): Pair<String, String> {
212   val originalIndent = getIndent(start, lookup)
213   val suffix = !originalIndent.all { it.isWhitespace() }
214   val indent =
215       if (suffix) {
216         originalIndent.map { if (it.isWhitespace()) it else ' ' }.joinToString(separator = "")
217       } else {
218         originalIndent
219       }
220 
221   val secondaryIndent =
222       if (suffix) {
223         // We don't have great heuristics to figure out what the indent should be
224         // following a source line -- e.g. it can be implied by things like whether
225         // the line ends with '{' or an operator, but it's more complicated than
226         // that. So we'll cheat and just look to see what the existing code does!
227         var offset = start
228         while (offset < max && lookup(offset) != '\n') {
229           offset++
230         }
231         offset++
232         val sb = StringBuilder()
233         while (offset < max) {
234           if (lookup(offset) == '\n') {
235             sb.clear()
236           } else {
237             val c = lookup(offset)
238             if (c.isWhitespace()) {
239               sb.append(c)
240             } else {
241               if (c == '*') {
242                 // in a comment, the * is often one space indented
243                 // to line up with the first * in the opening /** and
244                 // the actual indent should be aligned with the /
245                 sb.setLength(sb.length - 1)
246               }
247               break
248             }
249           }
250           offset++
251         }
252         sb.toString()
253       } else {
254         originalIndent
255       }
256 
257   return Pair(indent, secondaryIndent)
258 }
259 
260 /**
261  * Attempt to preserve the caret position across reformatting. Returns the delta in the new comment.
262  */
findSamePositionnull263 fun findSamePosition(comment: String, delta: Int, reformattedComment: String): Int {
264   // First see if the two comments are identical up to the delta; if so, same
265   // new position
266   for (i in 0 until min(comment.length, reformattedComment.length)) {
267     if (i == delta) {
268       return delta
269     } else if (comment[i] != reformattedComment[i]) {
270       break
271     }
272   }
273 
274   var i = comment.length - 1
275   var j = reformattedComment.length - 1
276   if (delta == i + 1) {
277     return j + 1
278   }
279   while (i >= 0 && j >= 0) {
280     if (i == delta) {
281       return j
282     }
283     if (comment[i] != reformattedComment[j]) {
284       break
285     }
286     i--
287     j--
288   }
289 
290   fun isSignificantChar(c: Char): Boolean = c.isWhitespace() || c == '*'
291 
292   // Finally it's somewhere in the middle; search by character skipping over
293   // insignificant characters (space, *, etc)
294   fun nextSignificantChar(s: String, from: Int): Int {
295     var curr = from
296     while (curr < s.length) {
297       val c = s[curr]
298       if (isSignificantChar(c)) {
299         curr++
300       } else {
301         break
302       }
303     }
304     return curr
305   }
306 
307   var offset = 0
308   var reformattedOffset = 0
309   while (offset < delta && reformattedOffset < reformattedComment.length) {
310     offset = nextSignificantChar(comment, offset)
311     reformattedOffset = nextSignificantChar(reformattedComment, reformattedOffset)
312     if (offset == delta) {
313       return reformattedOffset
314     }
315     offset++
316     reformattedOffset++
317   }
318   return reformattedOffset
319 }
320 
321 // Until stdlib version is no longer experimental
maxOfnull322 fun <T, R : Comparable<R>> Iterable<T>.maxOf(selector: (T) -> R): R {
323   val iterator = iterator()
324   if (!iterator.hasNext()) throw NoSuchElementException()
325   var maxValue = selector(iterator.next())
326   while (iterator.hasNext()) {
327     val v = selector(iterator.next())
328     if (maxValue < v) {
329       maxValue = v
330     }
331   }
332   return maxValue
333 }
334