├── cleanup.gif ├── modify-line.gif ├── screenshot.png ├── lint.xml ├── gradle-plugin ├── settings.gradle ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── gradle-plugins │ │ │ └── kdocformatter.properties │ │ └── kotlin │ │ └── kdocformatter │ │ └── gradle │ │ ├── KDocFormatterExtension.kt │ │ ├── KDocFormatterPlugin.kt │ │ └── KDocFormatterTask.kt ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── gradle.properties ├── screenshot-settings.png ├── cli ├── src │ ├── main │ │ └── kotlin │ │ │ ├── META-INF │ │ │ └── MANIFEST.MF │ │ │ └── kdocformatter │ │ │ └── cli │ │ │ ├── RangeFilter.kt │ │ │ ├── UnionFilter.kt │ │ │ ├── Driver.kt │ │ │ ├── LineRangeFilter.kt │ │ │ ├── GitRangeFilter.kt │ │ │ ├── EditorConfigs.kt │ │ │ ├── KDocFileFormatter.kt │ │ │ └── KDocFileFormattingOptions.kt │ └── test │ │ └── kotlin │ │ └── kdocformatter │ │ └── cli │ │ └── EditorConfigsTest.kt └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── ide-plugin ├── qodana.yml ├── lint-baseline.xml ├── README.md ├── gradle.properties ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ ├── plugin.xml │ │ │ └── pluginIcon.svg │ │ └── kotlin │ │ └── kdocformatter │ │ └── plugin │ │ ├── KDocPluginOptions.kt │ │ ├── KDocPostFormatProcessor.kt │ │ └── KDocOptionsConfigurable.kt ├── build.gradle.kts └── CHANGELOG.md ├── settings.gradle ├── .github └── workflows │ └── basic.yml ├── library ├── src │ ├── main │ │ ├── resources │ │ │ └── version.properties │ │ └── kotlin │ │ │ └── com │ │ │ └── facebook │ │ │ └── ktfmt │ │ │ └── kdoc │ │ │ ├── Version.kt │ │ │ ├── ParagraphList.kt │ │ │ ├── CommentType.kt │ │ │ ├── FormattingTask.kt │ │ │ ├── KDocFormattingOptions.kt │ │ │ ├── KDocFormatter.kt │ │ │ ├── Table.kt │ │ │ └── Utilities.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── facebook │ │ └── ktfmt │ │ └── kdoc │ │ ├── UtilitiesTest.kt │ │ └── DokkaVerifier.kt └── build.gradle ├── version.gradle ├── gradlew.bat ├── gradlew ├── README.md ├── LICENSE └── CHANGELOG.md /cleanup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/cleanup.gif -------------------------------------------------------------------------------- /modify-line.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/modify-line.gif -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/screenshot.png -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle-plugin/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kdoc-formatter' 2 | include 'gradle-plugin' 3 | -------------------------------------------------------------------------------- /gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | .idea 3 | .gradle 4 | build 5 | hs_err_pid* 6 | TODO.md 7 | m2 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 3 | -------------------------------------------------------------------------------- /screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/screenshot-settings.png -------------------------------------------------------------------------------- /cli/src/main/kotlin/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: kdocformatter.cli.Main 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle-plugin/src/main/resources/META-INF/gradle-plugins/kdocformatter.properties: -------------------------------------------------------------------------------- 1 | implementation-class=kdocformatter.gradle.KDocFormatterPlugin 2 | -------------------------------------------------------------------------------- /gradle-plugin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnorbye/kdoc-formatter/HEAD/gradle-plugin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | .idea 3 | .gradle 4 | build 5 | hs_err_pid* 6 | TODO.md 7 | m2 8 | *.swp 9 | .intellijPlatform 10 | .kotlin 11 | local.properties 12 | -------------------------------------------------------------------------------- /ide-plugin/qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | profile: 6 | name: qodana.recommended 7 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/kdocformatter/gradle/KDocFormatterExtension.kt: -------------------------------------------------------------------------------- 1 | package kdocformatter.gradle 2 | 3 | open class KDocFormatterExtension { 4 | /** Set arbitrary flags to pass to the KDoc formatting task. */ 5 | var options = "" 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradle-plugin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" 3 | } 4 | 5 | rootProject.name = 'kdoc-formatter' 6 | include 'cli' 7 | include 'library' 8 | include 'ide-plugin' 9 | // Including this in the same project causes gradle sync 10 | // to repeated sources which makes the IDE experience poor. 11 | // Instead this is now a separate project. 12 | //include 'gradle-plugin' 13 | -------------------------------------------------------------------------------- /.github/workflows/basic.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Validate gradle wrapper 12 | uses: gradle/wrapper-validation-action@v1 13 | - name: Set up JDK 17 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 17 17 | - name: Build with Gradle 18 | run: ./gradlew build test 19 | -------------------------------------------------------------------------------- /ide-plugin/lint-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/kdocformatter/gradle/KDocFormatterPlugin.kt: -------------------------------------------------------------------------------- 1 | package kdocformatter.gradle 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.JavaBasePlugin 6 | 7 | class KDocFormatterPlugin : Plugin { 8 | override fun apply(project: Project) { 9 | project.extensions.add("kdocformatter", KDocFormatterExtension::class.java) 10 | project.tasks.register("format-kdoc", KDocFormatterTask::class.java) { task -> 11 | task.description = "Format the Kotlin source code with the kdoc-formatter" 12 | task.group = JavaBasePlugin.DOCUMENTATION_GROUP 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /library/src/main/resources/version.properties: -------------------------------------------------------------------------------- 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 | # Release version definition 18 | buildVersion = 1.6.9 19 | -------------------------------------------------------------------------------- /version.gradle: -------------------------------------------------------------------------------- 1 | Properties properties = new Properties() 2 | File versionRootDir = rootDir 3 | String relativeVersionPath = "library/src/main/resources/version.properties" 4 | File versionProperties = new File(versionRootDir, relativeVersionPath) 5 | 6 | project.ext['versionProperties'] = versionProperties 7 | 8 | while (!versionProperties.exists() && versionRootDir.parentFile != null && 9 | versionRootDir.parentFile.exists()) { 10 | versionRootDir = versionRootDir.parentFile 11 | versionProperties = new File(versionRootDir, relativeVersionPath) 12 | } 13 | 14 | versionProperties.withReader { properties.load(it) } 15 | 16 | for (name in properties.stringPropertyNames()) { 17 | String version = properties.getProperty(name) 18 | project.ext[name] = version 19 | } 20 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' 4 | id 'com.android.lint' 5 | id 'com.ncorti.ktfmt.gradle' 6 | } 7 | 8 | group = "kdoc-formatter" 9 | version = rootProject.ext.buildVersion 10 | 11 | repositories { 12 | google() 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | 17 | lint { 18 | disable 'JavaPluginLanguageLevel' 19 | textReport true 20 | } 21 | 22 | dependencies { 23 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 24 | testImplementation "junit:junit:4.13.2" 25 | testImplementation "com.google.truth:truth:1.4.5" 26 | } 27 | 28 | java { 29 | toolchain { 30 | languageVersion = JavaLanguageVersion.of(17) 31 | } 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(17) 36 | } 37 | -------------------------------------------------------------------------------- /ide-plugin/README.md: -------------------------------------------------------------------------------- 1 | # KDoc Formatter Plugin 2 | 3 | This plugin setup was based on 4 | https://github.com/JetBrains/intellij-platform-plugin-template 5 | at revision 7251596f1644f6bb5a7b985e3e8ce0614826eb43. 6 | 7 | This plugin lets you reformat KDoc text -- meaning that it will reformat 8 | the text and flow the text up to the line width, collapsing comments 9 | that fit on a single line, indenting text within a block tag, etc. 10 | 11 | By default, it integrates into the IDE's formatting action, so you can 12 | invoke the formatter (e.g. Code | Reformat Code) and the comments will 13 | be handled by this plugin. 14 | 15 | There's a Settings panel which lets you configure the formatter to 16 | decide whether you want it to reorder KDoc tags to match the signature 17 | order, whether to align table columns, whether to convert HTML markup 18 | into equivalent KDoc markup, etc. 19 | 20 | More details about the features can be found at 21 | [https://github.com/tnorbye/kdoc-formatter#kdoc-formatter](https://github.com/tnorbye/kdoc-formatter#kdoc-formatter). 22 | -------------------------------------------------------------------------------- /ide-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = kdoc-formatter 5 | pluginName = kdoc-formatter-ide-plugin 6 | pluginRepositoryUrl = https://github.com/tnorbye/kdoc-formatter 7 | 8 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | # for insight into build numbers and IntelliJ Platform versions. 10 | pluginSinceBuild = 243 11 | pluginUntilBuild = 253.* 12 | 13 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 14 | platformType = IC 15 | platformVersion = 2025.2 16 | #platformVersion = 253.28294.334 17 | 18 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 19 | platformPlugins = org.jetbrains.kotlin,com.intellij.java,org.intellij.intelliLang 20 | 21 | # Java language level used to compile sources and to generate the files for 22 | javaVersion = 17 23 | 24 | # Opt-out flag for bundling Kotlin standard library. 25 | kotlin.stdlib.default.dependency=false 26 | -------------------------------------------------------------------------------- /cli/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.kotlin.jvm' 4 | id 'application' 5 | id 'com.android.lint' 6 | id 'com.ncorti.ktfmt.gradle' 7 | } 8 | 9 | group = "kdoc-formatter" 10 | version = rootProject.ext.buildVersion 11 | 12 | application { 13 | mainClass = 'kdocformatter.cli.Main' 14 | applicationName = "kdoc-formatter" 15 | } 16 | 17 | repositories { 18 | google() 19 | mavenCentral() 20 | gradlePluginPortal() 21 | } 22 | 23 | dependencies { 24 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 25 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4' 26 | testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4' 27 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 28 | implementation project(':library') 29 | } 30 | 31 | lint { 32 | disable 'JavaPluginLanguageLevel' 33 | textReport true 34 | } 35 | 36 | test { 37 | useJUnitPlatform() 38 | } 39 | 40 | java { 41 | toolchain { 42 | languageVersion = JavaLanguageVersion.of(17) 43 | } 44 | } 45 | 46 | kotlin { 47 | jvmToolchain(17) 48 | } 49 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/Version.kt: -------------------------------------------------------------------------------- 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.io.BufferedInputStream 20 | import java.util.Properties 21 | 22 | object Version { 23 | var versionString: String 24 | 25 | init { 26 | val properties = Properties() 27 | val stream = Version::class.java.getResourceAsStream("/version.properties") 28 | BufferedInputStream(stream).use { buffered -> properties.load(buffered) } 29 | versionString = properties.getProperty("buildVersion") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/kdocformatter/gradle/KDocFormatterTask.kt: -------------------------------------------------------------------------------- 1 | package kdocformatter.gradle 2 | 3 | import java.io.File 4 | import kdocformatter.cli.KDocFileFormatter 5 | import kdocformatter.cli.KDocFileFormattingOptions 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.plugins.JavaPluginConvention 8 | import org.gradle.api.tasks.TaskAction 9 | 10 | open class KDocFormatterTask : DefaultTask() { 11 | @TaskAction 12 | fun action() { 13 | val convention = convention.findPlugin(JavaPluginConvention::class.java) 14 | val dirs = 15 | convention?.sourceSets?.map { it.allSource }?.map { it.outputDir }?.toList() 16 | ?: listOf(File(".")) 17 | 18 | val extension = project.extensions.findByName("kdocformatter") as KDocFormatterExtension 19 | val flags = extension.options 20 | val args = flags.split(" ").toTypedArray() 21 | val options = KDocFileFormattingOptions.parse(args) 22 | val formatter = KDocFileFormatter(options) 23 | var count = 0 24 | for (file in dirs) { 25 | count += formatter.formatFile(file) 26 | } 27 | 28 | if (!options.quiet) { 29 | println("Formatted $count files") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/ParagraphList.kt: -------------------------------------------------------------------------------- 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 | /** 20 | * A list of paragraphs. Each paragraph should start on a new line and end with a newline. In 21 | * addition, if a paragraph is marked with "separate=true", we'll insert an extra blank line in 22 | * front of it. 23 | */ 24 | class ParagraphList(private val paragraphs: List) : Iterable { 25 | fun isSingleParagraph() = paragraphs.size <= 1 26 | 27 | override fun iterator(): Iterator = paragraphs.iterator() 28 | 29 | override fun toString(): String = paragraphs.joinToString { it.content } 30 | } 31 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/RangeFilter.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import java.io.File 20 | 21 | /** Filter to decide whether given text regions should be included. */ 22 | open class RangeFilter { 23 | /** 24 | * Return true if the range in [file] containing the contents [source] overlaps the range from 25 | * [startOffset] inclusive to [endOffset] exclusive. 26 | */ 27 | open fun overlaps(file: File, source: String, startOffset: Int, endOffset: Int): Boolean = true 28 | 29 | /** Returns true if the given file might include ranges that can return true from [overlaps]. */ 30 | open fun includes(file: File) = true 31 | 32 | /** Returns true if this filter is completely empty so nothing will match. */ 33 | open fun isEmpty(): Boolean = false 34 | } 35 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/UnionFilter.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import java.io.File 20 | 21 | class UnionFilter(private val filters: List) : RangeFilter() { 22 | override fun overlaps(file: File, source: String, startOffset: Int, endOffset: Int): Boolean { 23 | for (filter in filters) { 24 | if (filter.overlaps(file, source, startOffset, endOffset)) { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | override fun includes(file: File): Boolean { 32 | for (filter in filters) { 33 | if (filter.includes(file)) { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | override fun isEmpty(): Boolean { 41 | for (filter in filters) { 42 | if (!filter.isEmpty()) { 43 | return false 44 | } 45 | } 46 | return true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/Driver.kt: -------------------------------------------------------------------------------- 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 | @file:JvmName("Main") 18 | 19 | package kdocformatter.cli 20 | 21 | import kdocformatter.cli.KDocFileFormattingOptions.Companion.usage 22 | import kotlin.system.exitProcess 23 | 24 | fun main(args: Array) { 25 | if (args.isEmpty()) { 26 | println(usage()) 27 | exitProcess(-1) 28 | } 29 | 30 | val options = KDocFileFormattingOptions.parse(args) 31 | val files = options.files 32 | if (files.isEmpty()) { 33 | error("no files were provided") 34 | } else if (options.filter.isEmpty()) { 35 | if (!options.quiet && (options.gitStaged || options.gitHead)) { 36 | println( 37 | "No changes to Kotlin files found in ${ 38 | if (options.gitStaged) "the staged files" else "HEAD" 39 | }") 40 | } 41 | exitProcess(0) 42 | } 43 | val formatter = KDocFileFormatter(options) 44 | 45 | var count = 0 46 | for (file in files) { 47 | count += formatter.formatFile(file) 48 | } 49 | 50 | if (!options.quiet) { 51 | println("Formatted $count files") 52 | } 53 | 54 | exitProcess(0) 55 | } 56 | 57 | fun error(message: String): Nothing { 58 | System.err.println(message) 59 | System.err.println() 60 | System.err.println(usage()) 61 | exitProcess(-1) 62 | } 63 | -------------------------------------------------------------------------------- /ide-plugin/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | org.norbye.tor.kdocformatter 3 | Kotlin KDoc Formatter 4 | Tor Norbye 5 | 6 | 8 | com.intellij.modules.platform 9 | org.jetbrains.kotlin 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/CommentType.kt: -------------------------------------------------------------------------------- 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 | enum class CommentType( 20 | /** The opening string of the comment. */ 21 | val prefix: String, 22 | /** The closing string of the comment. */ 23 | val suffix: String, 24 | /** For multi line comments, the prefix at each comment line after the first one. */ 25 | val linePrefix: String 26 | ) { 27 | KDOC("/**", "*/", " * "), 28 | BLOCK("/*", "*/", ""), 29 | LINE("//", "", "// "); 30 | 31 | /** 32 | * The number of characters needed to fit a comment on a line: the prefix, suffix and a single 33 | * space padding inside these. 34 | */ 35 | fun singleLineOverhead(): Int { 36 | return prefix.length + suffix.length + 1 + if (suffix.isEmpty()) 0 else 1 37 | } 38 | 39 | /** 40 | * The number of characters required in addition to the line comment for each line in a multi line 41 | * comment. 42 | */ 43 | fun lineOverhead(): Int { 44 | return linePrefix.length 45 | } 46 | } 47 | 48 | fun String.isKDocComment(): Boolean = startsWith("/**") 49 | 50 | fun String.isBlockComment(): Boolean = startsWith("/*") && !startsWith("/**") 51 | 52 | fun String.isLineComment(): Boolean = startsWith("//") 53 | 54 | fun String.commentType(): CommentType { 55 | return if (isKDocComment()) { 56 | CommentType.KDOC 57 | } else if (isBlockComment()) { 58 | CommentType.BLOCK 59 | } else if (isLineComment()) { 60 | CommentType.LINE 61 | } else { 62 | error("Not a comment: $this") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/FormattingTask.kt: -------------------------------------------------------------------------------- 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 | class FormattingTask( 20 | /** Options to format with */ 21 | var options: KDocFormattingOptions, 22 | 23 | /** The original comment to be formatted */ 24 | var comment: String, 25 | 26 | /** 27 | * The initial indentation on the first line of the KDoc. The reformatted comment will prefix 28 | * each subsequent line with this string. 29 | */ 30 | var initialIndent: String, 31 | 32 | /** 33 | * Indent to use after the first line. 34 | * 35 | * This is useful when the comment starts the end of an existing code line. For example, 36 | * something like this: 37 | * ``` 38 | * if (foo.bar.baz()) { // This comment started at column 25 39 | * // but the second and subsequent lines are indented 8 spaces 40 | * // ... 41 | * ``` 42 | * 43 | * (This doesn't matter much for KDoc comments, since the formatter will always push these into 44 | * their own lines so the indents will match, but for line and block comments it can matter.) 45 | */ 46 | var secondaryIndent: String = initialIndent, 47 | 48 | /** 49 | * Optional list of parameters associated with this doc; if set, and if 50 | * [KDocFormattingOptions.orderDocTags] is set, parameter doc tags will be sorted to match this 51 | * order. (The intent is for the tool invoking KDocFormatter to pass in the parameter names in 52 | * signature order here.) 53 | */ 54 | var orderedParameterNames: List = emptyList(), 55 | 56 | /** The type of comment being formatted. */ 57 | val type: CommentType = comment.commentType() 58 | ) 59 | -------------------------------------------------------------------------------- /ide-plugin/src/main/kotlin/kdocformatter/plugin/KDocPluginOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2012 JetBrains s.r.o. 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 | package kdocformatter.plugin 17 | 18 | import com.facebook.ktfmt.kdoc.KDocFormattingOptions 19 | import com.intellij.openapi.application.ApplicationManager 20 | import com.intellij.openapi.components.PersistentStateComponent 21 | import com.intellij.openapi.components.State 22 | import com.intellij.openapi.components.Storage 23 | 24 | @State(name = "KDocFormatter", storages = [Storage("kdocFormatter.xml")]) 25 | class KDocPluginOptions : PersistentStateComponent { 26 | var globalState = GlobalState() 27 | private set 28 | 29 | override fun getState(): ComponentState { 30 | val state = ComponentState() 31 | state.state = globalState 32 | return state 33 | } 34 | 35 | override fun loadState(state: ComponentState) { 36 | globalState = state.state 37 | } 38 | 39 | class ComponentState { 40 | var state = GlobalState() 41 | } 42 | 43 | class GlobalState { 44 | private val defaults = KDocFormattingOptions() 45 | 46 | var collapseSingleLines = defaults.collapseSingleLine 47 | var convertMarkup = defaults.convertMarkup 48 | var addPunctuation = defaults.addPunctuation 49 | var alignTableColumns = defaults.alignTableColumns 50 | var reorderDocTags = defaults.orderDocTags 51 | 52 | // IDE plugin specific options 53 | var alternateActions = false 54 | var lineComments = false 55 | var formatProcessor = true 56 | var maxCommentWidthEnabled = true 57 | 58 | var overrideLineWidth: Int = 0 59 | var overrideCommentWidth: Int = 0 60 | var overrideHangingIndent: Int = -1 61 | } 62 | 63 | companion object { 64 | val instance: KDocPluginOptions 65 | get() = ApplicationManager.getApplication().getService(KDocPluginOptions::class.java) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: "$rootDir/../version.gradle" 3 | 4 | ext { 5 | gradlePluginVersion = '8.6.0' 6 | } 7 | 8 | repositories { 9 | google() 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | dependencies { 14 | classpath "com.android.tools.build:gradle:$gradlePluginVersion" 15 | } 16 | } 17 | 18 | plugins { 19 | id 'java' 20 | id 'org.jetbrains.kotlin.jvm' version '2.1.21' 21 | id 'java-gradle-plugin' 22 | id 'maven-publish' 23 | id 'com.gradle.plugin-publish' version '1.3.1' 24 | id 'com.ncorti.ktfmt.gradle' version '0.22.0' 25 | } 26 | 27 | // https://issues.sonatype.org/browse/OSSRH-63191 28 | group = "com.github.tnorbye.kdoc-formatter" 29 | version = rootProject.ext.buildVersion 30 | 31 | repositories { 32 | google() 33 | mavenCentral() 34 | gradlePluginPortal() 35 | } 36 | 37 | // Instead of depending on project(":cli") below, we inline the 38 | // sources in the plugin such that we don't have to publish 39 | // separate artifacts for the library and cli modules 40 | sourceSets { 41 | main.java.srcDirs += '../cli/src/main/kotlin' 42 | main.java.srcDirs += '../library/src/main/kotlin' 43 | } 44 | 45 | dependencies { 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 47 | implementation gradleApi() 48 | } 49 | 50 | java { 51 | toolchain { 52 | languageVersion = JavaLanguageVersion.of(17) 53 | } 54 | } 55 | 56 | kotlin { 57 | jvmToolchain(17) 58 | } 59 | 60 | gradlePlugin { 61 | website = 'https://github.com/tnorbye/kdoc-formatter' 62 | vcsUrl = 'https://github.com/tnorbye/kdoc-formatter.git' 63 | 64 | plugins { 65 | kdocformatter { 66 | id = "kdoc-formatter" 67 | displayName = 'KDoc Formatting' 68 | description = 'Plugin which can reformat Kotlin KDoc comments' 69 | implementationClass = "kdocformatter.gradle.KDocFormatterPlugin" 70 | tags.set(['kotlin', 'kdoc', 'formatter', 'formatting']) 71 | } 72 | } 73 | } 74 | 75 | /* 76 | publishing { 77 | publications { 78 | pluginPublication (MavenPublication) { 79 | from components.java 80 | groupId "com.github.tnorbye.kdoc-formatter" 81 | artifactId "kdoc-formatter" 82 | //version project.version 83 | version rootProject.ext.buildVersion 84 | } 85 | } 86 | } 87 | */ 88 | 89 | publishing { 90 | repositories { 91 | maven { 92 | url "../m2" 93 | } 94 | } 95 | } 96 | 97 | clean.doFirst { 98 | delete "${rootDir}/../m2" 99 | } 100 | 101 | task all { 102 | dependsOn 'clean', 'publish' 103 | } 104 | -------------------------------------------------------------------------------- /ide-plugin/src/main/kotlin/kdocformatter/plugin/KDocPostFormatProcessor.kt: -------------------------------------------------------------------------------- 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 kdocformatter.plugin 18 | 19 | import com.facebook.ktfmt.kdoc.KDocFormatter 20 | import com.intellij.openapi.util.TextRange 21 | import com.intellij.psi.PsiElement 22 | import com.intellij.psi.PsiFile 23 | import com.intellij.psi.codeStyle.CodeStyleSettings 24 | import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor 25 | import com.intellij.psi.util.PsiTreeUtil 26 | import org.jetbrains.kotlin.kdoc.psi.api.KDoc 27 | import org.jetbrains.kotlin.psi.KtPsiFactory 28 | 29 | class KDocPostFormatProcessor : PostFormatProcessor { 30 | override fun processElement(source: PsiElement, settings: CodeStyleSettings): PsiElement { 31 | if (!KDocPluginOptions.instance.globalState.formatProcessor) { 32 | return source 33 | } 34 | 35 | val kdoc = source as? KDoc ?: return source 36 | 37 | // TODO: Consult options to see whether we want this to participate in 38 | // formatting 39 | val file = source.containingFile 40 | val original = kdoc.text 41 | val options = createFormattingOptions(file, source, false) 42 | val task = createFormattingTask(source, original, options) 43 | val formatted = KDocFormatter(options).reformatComment(task) 44 | return if (formatted != original) { 45 | val newComment = KtPsiFactory(source.project).createComment(formatted) 46 | return source.replace(newComment) 47 | } else { 48 | source 49 | } 50 | } 51 | 52 | override fun processText( 53 | source: PsiFile, 54 | rangeToReformat: TextRange, 55 | settings: CodeStyleSettings 56 | ): TextRange { 57 | // Format all top-level comments in this range 58 | for (element in PsiTreeUtil.findChildrenOfType(source, KDoc::class.java)) { 59 | if (rangeToReformat.intersects(element.textRange)) { 60 | processElement(element, settings) 61 | } 62 | } 63 | return rangeToReformat 64 | } 65 | 66 | private fun getIndent(file: PsiFile, offset: Int): String { 67 | val documentText = file.text 68 | var curr = offset - 1 69 | while (curr >= 0) { 70 | val c = documentText[curr] 71 | if (c == '\n') { 72 | break 73 | } else if (!c.isWhitespace()) { 74 | // No indent 75 | curr = offset - 1 76 | break 77 | } else { 78 | curr-- 79 | } 80 | } 81 | return documentText.substring(curr + 1, offset) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /gradle-plugin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/LineRangeFilter.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import com.facebook.ktfmt.kdoc.getLineNumber 20 | import java.io.File 21 | 22 | open class LineRangeFilter protected constructor(private val rangeMap: RangeMap) : RangeFilter() { 23 | var valid: Boolean = false 24 | 25 | override fun isEmpty(): Boolean = rangeMap.isEmpty() 26 | 27 | override fun includes(file: File): Boolean { 28 | return rangeMap.getRanges(file).isNotEmpty() 29 | } 30 | 31 | override fun overlaps(file: File, source: String, startOffset: Int, endOffset: Int): Boolean { 32 | val startLine = getLineNumber(source, startOffset) 33 | val endLine = getLineNumber(source, endOffset, startLine, startOffset) 34 | val ranges = rangeMap.getRanges(file) 35 | for (range in ranges) { 36 | if (range.overlaps(startLine, endLine)) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | 44 | protected class Range( 45 | private val startLine: Int, // inclusive 46 | private val endLine: Int // exclusive 47 | ) { 48 | fun overlaps(startLine: Int, endLine: Int): Boolean { 49 | return this.startLine <= endLine && this.endLine >= startLine 50 | } 51 | 52 | override fun toString(): String { 53 | return "From $startLine to $endLine" 54 | } 55 | } 56 | 57 | protected class RangeMap { 58 | private val fileToRanges = HashMap>() 59 | 60 | fun getRanges(file: File): List { 61 | return fileToRanges[file] ?: emptyList() 62 | } 63 | 64 | fun isEmpty(): Boolean { 65 | return fileToRanges.isEmpty() 66 | } 67 | 68 | fun addRange(file: File, startLine: Int, endLine: Int) { 69 | val list = fileToRanges[file] ?: ArrayList().also { fileToRanges[file] = it } 70 | list.add(Range(startLine, endLine)) 71 | } 72 | } 73 | 74 | companion object { 75 | fun fromRangeStrings(file: File, rangeStrings: List): LineRangeFilter { 76 | val rangeMap = RangeMap() 77 | 78 | val filter = LineRangeFilter(rangeMap) 79 | for (rangeString in rangeStrings) { 80 | for (s in rangeString.split(",")) { 81 | val endSeparator = s.indexOf(':') 82 | if (endSeparator == -1) { 83 | val line = s.toIntOrNull() ?: error("Line $s is not a number") 84 | rangeMap.addRange(file, line, line + 1) 85 | } else { 86 | val startString = s.substring(0, endSeparator) 87 | val endString = s.substring(endSeparator + 1) 88 | val start = startString.toIntOrNull() ?: error("Line $startString is not a number") 89 | val end = endString.toIntOrNull() ?: error("Line $endString is not a number") 90 | // ranges operate with endLine is exclusive, but command line option is 91 | // inclusive 92 | rangeMap.addRange(file, start, end + 1) 93 | } 94 | } 95 | } 96 | filter.valid = true 97 | return LineRangeFilter(rangeMap) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ide-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | import org.jetbrains.changelog.Changelog 3 | import org.jetbrains.changelog.markdownToHTML 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 6 | 7 | private fun properties(key: String) = project.findProperty(key).toString() 8 | 9 | plugins { 10 | id("java") 11 | id("org.jetbrains.kotlin.jvm") 12 | id("org.jetbrains.intellij.platform") version "2.10.5" 13 | id("org.jetbrains.changelog") version "2.5.0" 14 | id("com.android.lint") 15 | id("com.ncorti.ktfmt.gradle") 16 | } 17 | 18 | val pluginVersion: String = 19 | Properties() 20 | .apply { load(file("../library/src/main/resources/version.properties").inputStream()) } 21 | .getProperty("buildVersion") 22 | 23 | group = properties("pluginGroup") 24 | 25 | version = pluginVersion 26 | 27 | repositories { 28 | google() 29 | mavenCentral() 30 | intellijPlatform { defaultRepositories() } 31 | } 32 | 33 | intellijPlatform { pluginConfiguration { name = properties("pluginName") } } 34 | 35 | changelog { 36 | version.set(pluginVersion) 37 | groups.set(emptyList()) 38 | repositoryUrl.set(properties("pluginRepositoryUrl")) 39 | } 40 | 41 | tasks { 42 | properties("javaVersion").let { 43 | withType { 44 | sourceCompatibility = it 45 | targetCompatibility = it 46 | } 47 | withType { compilerOptions.jvmTarget.set(JvmTarget.fromTarget(it)) } 48 | } 49 | 50 | patchPluginXml { 51 | version = pluginVersion.get() 52 | sinceBuild.set(properties("pluginSinceBuild")) 53 | untilBuild.set(properties("pluginUntilBuild")) 54 | 55 | pluginDescription.set( 56 | projectDir 57 | .resolve("README.md") 58 | .readText() 59 | .lines() 60 | .run { 61 | val start = "" 62 | val end = "" 63 | 64 | if (!containsAll(listOf(start, end))) { 65 | throw GradleException( 66 | "Plugin description section not found in README.md:\n$start ... $end") 67 | } 68 | subList(indexOf(start) + 1, indexOf(end)) 69 | } 70 | .joinToString("\n") 71 | .run { markdownToHTML(this) }) 72 | 73 | // Get the latest available change notes from the changelog file 74 | changeNotes.set( 75 | provider { 76 | with(changelog) { 77 | renderItem( 78 | getOrNull(properties("pluginVersion")) 79 | ?: runCatching { getLatest() }.getOrElse { getUnreleased() }, 80 | Changelog.OutputType.HTML, 81 | ) 82 | } 83 | }) 84 | } 85 | 86 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 87 | val runIdeForUiTests by 88 | intellijPlatformTesting.runIde.registering { 89 | task { 90 | jvmArgumentProviders += CommandLineArgumentProvider { 91 | listOf( 92 | "-Drobot-server.port=8082", 93 | "-Dide.mac.message.dialogs.as.sheets=false", 94 | "-Djb.privacy.policy.text=", 95 | "-Djb.consents.confirmation.enabled=false", 96 | ) 97 | } 98 | } 99 | 100 | plugins { robotServerPlugin() } 101 | } 102 | } 103 | 104 | lint { 105 | textReport = true 106 | baseline = file("lint-baseline.xml") 107 | } 108 | 109 | dependencies { 110 | implementation(project(":library")) 111 | intellijPlatform { 112 | create(properties("platformType"), properties("platformVersion")) 113 | bundledPlugins(properties("platformPlugins").split(',')) 114 | } 115 | } 116 | 117 | defaultTasks("buildPlugin") 118 | -------------------------------------------------------------------------------- /ide-plugin/src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 69 | / 86 | 88 | 89 | 97 | 101 | 105 | 109 | 110 | 111 | 112 | 113 | * 126 | -------------------------------------------------------------------------------- /cli/src/test/kotlin/kdocformatter/cli/EditorConfigsTest.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import com.facebook.ktfmt.kdoc.KDocFormattingOptions 20 | import java.io.File 21 | import org.intellij.lang.annotations.Language 22 | import org.junit.jupiter.api.Assertions.assertEquals 23 | import org.junit.jupiter.api.Test 24 | import org.junit.jupiter.api.io.TempDir 25 | 26 | class EditorConfigsTest { 27 | companion object { 28 | @TempDir @JvmField var temporaryFolder: File? = null 29 | } 30 | 31 | class ConfigFile(val relativePath: String, @param:Language("EditorConfig") val contents: String) 32 | 33 | private fun createFileTree(vararg files: ConfigFile): File { 34 | val root = temporaryFolder!! 35 | root.deleteRecursively() 36 | for (file in files) { 37 | val target = File(root, file.relativePath) 38 | target.parentFile?.mkdirs() 39 | target.writeText(file.contents) 40 | } 41 | return root 42 | } 43 | 44 | @Test 45 | fun testBasics() { 46 | EditorConfigs.root = null 47 | val fileTree = 48 | createFileTree( 49 | ConfigFile( 50 | "root/.editorconfig", 51 | ";comment\nroot = true\n[*]\nmax_line_length=150\ntab_width = 10"), 52 | ConfigFile( 53 | "root/sub1/sub2/.editorconfig", 54 | "[*]\nindent_size = 6\n[*.md]\nmax_line_length = 40\n[*.kt]\nmax_line_length = 60"), 55 | ConfigFile( 56 | "root/sub1/sub3/.editorconfig", 57 | "[*]\nindent_size = 6\n[{*.java,*.kt}]\nmax_line_length=120\n[*.md]\nmax_line_length = 80\n; max_line_length = 110")) 58 | val file1 = File(fileTree, "root/sub1/sub2/sub3/sub4/foo.kt") 59 | val options1 = EditorConfigs.getOptions(file1) 60 | assertEquals(60, options1.maxLineWidth) 61 | assertEquals(40, options1.maxCommentWidth) 62 | 63 | val file2 = File(fileTree, "root/sub1/sub2/foo.kt") 64 | val options2 = EditorConfigs.getOptions(file2) 65 | assertEquals(60, options2.maxLineWidth) 66 | assertEquals(40, options2.maxCommentWidth) 67 | 68 | val file3 = File(fileTree, "root/sub1/foo.kt") 69 | val options3 = EditorConfigs.getOptions(file3) 70 | assertEquals(150, options3.maxLineWidth) 71 | assertEquals(KDocFormattingOptions().maxCommentWidth, options3.maxCommentWidth) 72 | 73 | val file4 = File(fileTree, "root/sub1/sub3/foo.kt") 74 | val options4 = EditorConfigs.getOptions(file4) 75 | assertEquals(120, options4.maxLineWidth) 76 | assertEquals(80, options4.maxCommentWidth) 77 | assertEquals(6, options4.hangingIndent) 78 | } 79 | 80 | @Test 81 | fun testFallback() { 82 | val fallback = KDocFormattingOptions() 83 | fallback.maxLineWidth = 80 84 | fallback.maxCommentWidth = 50 85 | EditorConfigs.root = fallback 86 | 87 | val fileTree = 88 | createFileTree( 89 | ConfigFile( 90 | "root/.editorconfig", "root = true\n[*]\nmax_line_length=150\ntab_width = 10")) 91 | val file1 = File(fileTree, "root/sub1/sub2/sub3/sub4/foo.kt") 92 | val options1 = EditorConfigs.getOptions(file1) 93 | assertEquals(150, options1.maxLineWidth) 94 | assertEquals(50, options1.maxCommentWidth) 95 | } 96 | 97 | @Test 98 | fun testUnset() { 99 | val fallback = KDocFormattingOptions() 100 | fallback.maxLineWidth = 80 101 | fallback.maxCommentWidth = 50 102 | EditorConfigs.root = fallback 103 | 104 | val fileTree = 105 | createFileTree( 106 | ConfigFile( 107 | "root/.editorconfig", "root = true\n[*]\nmax_line_length=150\ntab_width = 10"), 108 | ConfigFile( 109 | "root/sub1/sub2/.editorconfig", 110 | "[*]\nindent_size = 6\n[*.kt]\nmax_line_length = unset")) 111 | val file = File(fileTree, "root/sub1/sub2/sub3/sub4/foo.kt") 112 | val options = EditorConfigs.getOptions(file) 113 | assertEquals( 114 | 80, options.maxLineWidth) // go to fallback since set to unset in closest editor config 115 | } 116 | 117 | @Test 118 | fun testStopAtRoot() { 119 | val fallback = KDocFormattingOptions() 120 | fallback.maxLineWidth = 80 121 | fallback.maxCommentWidth = 50 122 | EditorConfigs.root = fallback 123 | 124 | val fileTree = 125 | createFileTree( 126 | ConfigFile( 127 | "root/.editorconfig", "root = true\n[*]\nmax_line_length=150\ntab_width = 10"), 128 | ConfigFile("root/sub1/sub2/.editorconfig", "root = true\n[*]\nindent_size = 6\n")) 129 | val file = File(fileTree, "root/sub1/sub2/sub3/sub4/foo.kt") 130 | val options = EditorConfigs.getOptions(file) 131 | assertEquals(80, options.maxLineWidth) // go to fallback since stops at local root 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Portions Copyright (c) Meta Platforms, Inc. and affiliates. 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 | /* 18 | * Copyright (c) Tor Norbye. 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | 33 | package com.facebook.ktfmt.kdoc 34 | 35 | import kotlin.math.min 36 | 37 | /** Options controlling how the [KDocFormatter] will behave. */ 38 | class KDocFormattingOptions( 39 | /** Right hand side margin to write lines at. */ 40 | var maxLineWidth: Int = 72, 41 | /** 42 | * Limit comment to be at most [maxCommentWidth] characters even if more would fit on the line. 43 | */ 44 | var maxCommentWidth: Int = min(maxLineWidth, 72), 45 | ) { 46 | /** Whether to collapse multi-line comments that would fit on a single line into a single line. */ 47 | var collapseSingleLine: Boolean = true 48 | 49 | /** Whether to collapse repeated spaces. */ 50 | var collapseSpaces: Boolean = true 51 | 52 | /** Whether to convert basic markup like **bold** into **bold**, < into <, etc. */ 53 | var convertMarkup: Boolean = true 54 | 55 | /** 56 | * Whether to add punctuation where missing, such as ending sentences with a period. (TODO: Make 57 | * sure the FIRST sentence ends with one too! Especially if the subsequent sentence is separated.) 58 | */ 59 | var addPunctuation: Boolean = false 60 | 61 | /** 62 | * How many spaces to use for hanging indents in numbered lists and after block tags. Using 4 or 63 | * more here will result in subsequent lines being interpreted as block formatted by IntelliJ (but 64 | * not Dokka). 65 | */ 66 | var hangingIndent: Int = 3 67 | 68 | /** When there are nested lists etc, how many spaces to indent by. */ 69 | var nestedListIndent: Int = 3 70 | set(value) { 71 | if (value < 3) { 72 | error( 73 | "Nested list indent must be at least 3; if list items are only indented 2 spaces they " + 74 | "will not be rendered as list items") 75 | } 76 | field = value 77 | } 78 | 79 | /** 80 | * Don't format with tabs! (See 81 | * https://kotlinlang.org/docs/reference/coding-conventions.html#formatting) 82 | * 83 | * But if you do, this is the tab width. 84 | */ 85 | var tabWidth: Int = 8 86 | 87 | /** Whether to perform optimal line breaking instead of greeding. */ 88 | var optimal: Boolean = true 89 | 90 | /** 91 | * If true, reformat markdown tables such that the column markers line up. When false, markdown 92 | * tables are left alone (except for left hand side cleanup.) 93 | */ 94 | var alignTableColumns: Boolean = true 95 | 96 | /** 97 | * If true, moves any kdoc tags to the end of the comment and `@return` tags after `@param` tags. 98 | */ 99 | var orderDocTags: Boolean = true 100 | 101 | /** 102 | * If true, perform "alternative" formatting. This is only relevant in the IDE. You can invoke the 103 | * action repeatedly and it will jump between normal formatting an alternative formatting. For 104 | * single-line comments it will alternate between single and multiple lines. For longer comments 105 | * it will alternate between optimal line breaking and greedy line breaking. 106 | */ 107 | var alternate: Boolean = false 108 | 109 | /** 110 | * KDoc allows param tag to be specified using an alternate bracket syntax. KDoc formatter ties to 111 | * unify the format of comments, so it will rewrite them into the canonical syntax unless this 112 | * option is true. 113 | */ 114 | var allowParamBrackets: Boolean = false 115 | 116 | /** Creates a copy of this formatting object. */ 117 | fun copy(): KDocFormattingOptions { 118 | val copy = KDocFormattingOptions() 119 | copy.maxLineWidth = maxLineWidth 120 | copy.maxCommentWidth = maxCommentWidth 121 | copy.collapseSingleLine = collapseSingleLine 122 | copy.collapseSpaces = collapseSpaces 123 | copy.hangingIndent = hangingIndent 124 | copy.tabWidth = tabWidth 125 | copy.alignTableColumns = alignTableColumns 126 | copy.orderDocTags = orderDocTags 127 | copy.addPunctuation = addPunctuation 128 | copy.convertMarkup = convertMarkup 129 | copy.nestedListIndent = nestedListIndent 130 | copy.optimal = optimal 131 | copy.alternate = alternate 132 | 133 | return copy 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /ide-plugin/src/main/kotlin/kdocformatter/plugin/KDocOptionsConfigurable.kt: -------------------------------------------------------------------------------- 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 kdocformatter.plugin 18 | 19 | import com.intellij.openapi.options.Configurable 20 | import com.intellij.openapi.options.SearchableConfigurable 21 | import com.intellij.openapi.options.UiDslUnnamedConfigurable 22 | import com.intellij.openapi.ui.validation.DialogValidation 23 | import com.intellij.openapi.ui.validation.validationErrorIf 24 | import com.intellij.ui.dsl.builder.* 25 | import javax.swing.text.JTextComponent 26 | import kotlin.reflect.KMutableProperty0 27 | import org.jetbrains.annotations.Nls 28 | 29 | class KDocOptionsConfigurable : 30 | UiDslUnnamedConfigurable.Simple(), SearchableConfigurable, Configurable.NoScroll { 31 | @Nls override fun getDisplayName() = "KDoc Formatting" 32 | 33 | @Suppress("SpellCheckingInspection") override fun getId() = "kdocformatter.options" 34 | 35 | private val state = KDocPluginOptions.instance.globalState 36 | 37 | override fun Panel.createContent() { 38 | panel { 39 | row { 40 | checkBox("Collapse short comments that fit on a single line") 41 | .bindSelected(state::collapseSingleLines) 42 | } 43 | row { 44 | checkBox("Convert markup like bold into **bold**").bindSelected(state::convertMarkup) 45 | } 46 | row { 47 | checkBox("Align table columns, ensuring that | separators line up") 48 | .bindSelected(state::alignTableColumns) 49 | } 50 | row { 51 | checkBox("Move and reorder KDoc tags to match signature order") 52 | .bindSelected(state::reorderDocTags) 53 | } 54 | row { 55 | checkBox("Add missing punctuation, such as a period at the end of a capitalized paragraph") 56 | .bindSelected(state::addPunctuation) 57 | } 58 | separator() 59 | row { 60 | checkBox("Participate in IDE formatting operations, such as Code > Reformat Code") 61 | .bindSelected(state::formatProcessor) 62 | } 63 | row { 64 | checkBox( 65 | "Alternate line breaking algorithms when invoked repeatedly (between greedy and optimal)") 66 | .bindSelected(state::alternateActions) 67 | } 68 | row { 69 | checkBox("Allow formatting line comments and block comments interactively") 70 | .bindSelected(state::lineComments) 71 | } 72 | separator() 73 | row { 74 | checkBox("Allow max comment width to be separate from line width") 75 | .bindSelected(state::maxCommentWidthEnabled) 76 | } 77 | row { 78 | comment( 79 | "When checked, comments will be limited 72 characters (or the configured Markdown line length),\n" + 80 | "for improved readability. Otherwise, comments will use the full available line width.", 81 | ) 82 | } 83 | separator() 84 | row { 85 | label( 86 | "Override line widths (if blank or 0, the code style line width or .editorconfig is used):") 87 | } 88 | 89 | row("Line Width") { 90 | // Not using intTextField(range = 10..1000) because we want to allow blanks 91 | textField().bindWidth(state::overrideLineWidth).columns(4) 92 | } 93 | 94 | row("Comment Width") { 95 | textField() 96 | .bindWidth(state::overrideCommentWidth) 97 | .columns(4) 98 | .trimmedTextValidation(widthValidator) 99 | } 100 | 101 | separator() 102 | row { label("Override continuation indent (@param lists, etc); leave blank to use default:") } 103 | 104 | row("Continuation Indentation") { 105 | textField() 106 | .bindWidth(state::overrideHangingIndent, -1) 107 | .columns(4) 108 | .trimmedTextValidation(indentValidator) 109 | } 110 | } 111 | } 112 | 113 | private fun Cell.bindWidth( 114 | prop: KMutableProperty0, 115 | useDefault: Int = 0 116 | ): Cell { 117 | return bindWidth(prop.toMutableProperty(), useDefault) 118 | } 119 | 120 | // Like bindIntText, but treats blank as [default] 121 | private fun Cell.bindWidth( 122 | prop: MutableProperty, 123 | useDefault: Int = 0 124 | ): Cell { 125 | return bindText( 126 | getter = { 127 | val value = prop.get() 128 | if (value == useDefault) "" else value.toString() 129 | }, 130 | setter = { value: String -> 131 | if (value.isEmpty()) { 132 | prop.set(useDefault) 133 | } else { 134 | val v = value.toIntOrNull() 135 | if (v != null) { 136 | prop.set(v) 137 | } 138 | } 139 | }) 140 | } 141 | 142 | private val widthValidator: DialogValidation.WithParameter<() -> String> = 143 | validationErrorIf("Field must be empty or an integer in the range 10 to 1000") { 144 | val value = it 145 | value.isNotEmpty() && value.any { digit -> !digit.isDigit() } || 146 | (value.toIntOrNull() == null || value.toInt() < 10) 147 | } 148 | 149 | private val indentValidator: DialogValidation.WithParameter<() -> String> = 150 | validationErrorIf("Field must be empty or an integer in the range 0 to 12") { 151 | val value = it 152 | value.isNotEmpty() && value.any { digit -> !digit.isDigit() } || 153 | (value.toIntOrNull() == null || value.toInt() > 12) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/KDocFormatter.kt: -------------------------------------------------------------------------------- 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 kotlin.math.min 20 | 21 | /** Formatter which can reformat KDoc comments. */ 22 | class KDocFormatter(private val options: KDocFormattingOptions) { 23 | /** Reformats the [comment], which follows the given [initialIndent] string. */ 24 | fun reformatComment(comment: String, initialIndent: String): String { 25 | return reformatComment(FormattingTask(options, comment, initialIndent)) 26 | } 27 | 28 | fun reformatComment(task: FormattingTask): String { 29 | val indent = task.secondaryIndent 30 | val indentSize = getIndentSize(indent, options) 31 | val firstIndentSize = getIndentSize(task.initialIndent, options) 32 | val comment = task.comment 33 | val lineComment = comment.isLineComment() 34 | val blockComment = comment.isBlockComment() 35 | val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize) 36 | val commentType = task.type 37 | val lineSeparator = "\n$indent${commentType.linePrefix}" 38 | val prefix = commentType.prefix 39 | 40 | // Collapse single line? If alternate is turned on, use the opposite of the 41 | // setting 42 | val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it } 43 | if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) { 44 | // Does the text fit on a single line? 45 | val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: "" 46 | // Subtract out space for "/** " and " */" and the indent: 47 | val width = 48 | min( 49 | options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(), 50 | options.maxCommentWidth) 51 | val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}" 52 | if (trimmed.length <= width) { 53 | return "$prefix $trimmed$suffix" 54 | } 55 | if (indentSize < firstIndentSize) { 56 | val nextLineWidth = 57 | min( 58 | options.maxLineWidth - indentSize - commentType.singleLineOverhead(), 59 | options.maxCommentWidth) 60 | if (trimmed.length <= nextLineWidth) { 61 | return "$prefix $trimmed$suffix" 62 | } 63 | } 64 | } 65 | 66 | val sb = StringBuilder() 67 | 68 | sb.append(prefix) 69 | if (lineComment) { 70 | sb.append(' ') 71 | } else { 72 | sb.append(lineSeparator) 73 | } 74 | 75 | for (paragraph in paragraphs) { 76 | if (paragraph.separate) { 77 | // Remove trailing spaces which can happen when we have a paragraph 78 | // separator 79 | stripTrailingSpaces(lineComment, sb) 80 | sb.append(lineSeparator) 81 | } 82 | val text = paragraph.text 83 | if (paragraph.preformatted || paragraph.table) { 84 | sb.append(text) 85 | // Remove trailing spaces which can happen when we have an empty line in a 86 | // preformatted paragraph. 87 | stripTrailingSpaces(lineComment, sb) 88 | sb.append(lineSeparator) 89 | continue 90 | } 91 | 92 | val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead() 93 | val quoteAdjustment = if (paragraph.quoted) 2 else 0 94 | val maxLineWidth = 95 | min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment 96 | val firstMaxLineWidth = 97 | if (sb.indexOf('\n') == -1) { 98 | min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment 99 | } else { 100 | maxLineWidth 101 | } 102 | 103 | val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth) 104 | var first = true 105 | val hangingIndent = paragraph.hangingIndent 106 | for (line in lines) { 107 | sb.append(paragraph.indent) 108 | if (first && !paragraph.continuation) { 109 | first = false 110 | } else { 111 | sb.append(hangingIndent) 112 | } 113 | if (paragraph.quoted) { 114 | sb.append("> ") 115 | } 116 | if (line.isEmpty()) { 117 | // Remove trailing spaces which can happen when we have a paragraph 118 | // separator 119 | stripTrailingSpaces(lineComment, sb) 120 | } else { 121 | sb.append(line) 122 | } 123 | sb.append(lineSeparator) 124 | } 125 | } 126 | if (!lineComment) { 127 | if (sb.endsWith("* ")) { 128 | sb.setLength(sb.length - 2) 129 | } 130 | sb.append("*/") 131 | } else if (sb.endsWith(lineSeparator)) { 132 | @Suppress("ReturnValueIgnored") sb.removeSuffix(lineSeparator) 133 | } 134 | 135 | val formatted = 136 | if (lineComment) { 137 | sb.trim().removeSuffix("//").trim().toString() 138 | } else if (blockComment) { 139 | sb.toString().replace(lineSeparator + "\n", "\n\n") 140 | } else { 141 | sb.toString() 142 | } 143 | 144 | val separatorIndex = comment.indexOf('\n') 145 | return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') { 146 | // CRLF separator 147 | formatted.replace("\n", "\r\n") 148 | } else { 149 | formatted 150 | } 151 | } 152 | 153 | private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) { 154 | if (!lineComment && sb.endsWith("* ")) { 155 | sb.setLength(sb.length - 1) 156 | } else if (lineComment && sb.endsWith("// ")) { 157 | sb.setLength(sb.length - 1) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /ide-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # KDoc Formatter Plugin Changelog 4 | 5 | ## [1.6.9] 6 | - Add support for 2025.3 EAP 7 | 8 | ## [1.6.8] 9 | - Add support for IJP 2025.2 EAP 10 | 11 | ## [1.6.7] 12 | - Updated code to replace deprecated API usages 13 | - Fix issue #104: Include type parameters in parameter list reordering 14 | - Fix issue #105: Make add punctuation apply to all paragraphs, not just last 15 | 16 | ## [1.6.6] 17 | - Add support for 2025.1 EAP, and migrate to 2.x version of IntelliJ gradle plugin. 18 | - Fix https://github.com/tnorbye/kdoc-formatter/issues/106 19 | - Allow line comment block reformatting to work at the end of lines 20 | 21 | ## [1.6.5] 22 | - Mark plugin as compatible with 2024.3. 23 | 24 | ## [1.6.4] 25 | - Switch continuation indent from 4 to 3. (IntelliJ's Dokka preview 26 | treats an indent of 4 or more as preformatted text even on a continued 27 | line; Dokka itself (and Markdown) does not. 28 | - Add ability to override the continuation indent in the IDE plugin 29 | settings. 30 | - Don't reorder `@sample` tags (backported 31 | https://github.com/facebook/ktfmt/issues/406) 32 | 33 | ## [1.6.3] 34 | - Compatibility with IntelliJ 2024.2 EAP 35 | - Mark plugin as compatible with K2 36 | 37 | ## [1.6.2] 38 | - Compatibility with IntelliJ 2024.1 EAP. 39 | 40 | ## [1.6.1] 41 | - Compatibility with IntelliJ 2023.3 EAP. 42 | 43 | ## [1.6.0] 44 | - Updated dependencies and fixed a few minor bugs, including 45 | https://github.com/tnorbye/kdoc-formatter/issues/88 as well as issue 46 | 398 in ktfmt. 47 | 48 | ## [1.5.9] 49 | - Compatibility with IntelliJ 2023.1 50 | 51 | ## [1.5.8] 52 | - Fixed a number of bugs: 53 | - #84: Line overrun when using closed-open interval notation 54 | - More gracefully handle unterminated [] references (for example when 55 | comment is using it in things like [closed, open) intervals) 56 | - Recognize and convert accidentally capitalized kdoc tags like @See 57 | - If you have a [ref] which spans a line such that the # ends up as a 58 | new line, don't treat this as a "# heading". 59 | - If you're using optimal line breaking and there's a really long, 60 | unbreakable word in the paragraph, switch that paragraph over to 61 | greedy line breaking (to make the paragraph better balanced since 62 | the really long word throws the algorithm off.) 63 | - Fix a few scenarios where markup conversion from

and

64 | wasn't converting everything. 65 | - Allow @property[name], not just @param[name] 66 | - Some minor code cleanup. 67 | 68 | ## [1.5.7] 69 | - Fixed the following bugs: 70 | - #76: Preserve newline style (CRLF on Windows) 71 | - #77: Preformatting error 72 | - #78: Preformatting stability 73 | - #79: Replace `{@param name}` with `[name]` 74 | 75 | ## [1.5.6] 76 | - Bugfix: the override line width setting was not working 77 | 78 | ## [1.5.5] 79 | - The plugin can now be upgraded without restarting the IDE 80 | - Improved support for .editorconfig files; these settings will now be 81 | reflected immediately (in prior versions you had to restart the IDE 82 | because they were improperly cached) 83 | - Fixed a copy/paste bug which prevented the "Collapse short comments 84 | that fit on a single line" option from working. 85 | - Several formatting related improvements (fixes for 86 | bugs #53, #69, #70, #71, #72) 87 | 88 | ## [1.5.4] 89 | - Fix 9 bugs filed by the ktfmt project. 90 | 91 | ## [1.5.3] 92 | - @param tags are reordered to match the parameter order in the 93 | corresponding method signature. 94 | - There are now options for explicitly specifying the line width and the 95 | comment width which overrides the inferred width from code styles or 96 | .editorconfig files. 97 | - Some reorganization of the options along with updates labels to 98 | clarify what they mean. 99 | 100 | ## [1.5.2] 101 | - Adds a new option which lets you turn off the concept of a separate 102 | maximum comment width from the maximum line width. By default, 103 | comments are limited to 72 characters wide (or more accurately the 104 | configured width for Markdown files), which leads to more readable 105 | text. However, if you really want the full line width to be used, 106 | uncheck the "Allow max comment width to be separate from line width" 107 | setting. 108 | - Fixes bug to ensure the line width and max comment width are properly 109 | read from the IDE environment settings. 110 | 111 | ## [1.5.1] 112 | - Updated formatter with many bug fixes, as well as improved support for 113 | formatting tables as well as reordering KDoc tags. There are new 114 | options controlling both of these behaviors. 115 | - Removed Kotlin logo from the IDE plugin icon 116 | 117 | ## [1.4.1] 118 | - Fix formatting nested bulleted lists 119 | (https://github.com/tnorbye/kdoc-formatter/issues/36) 120 | 121 | ## [1.4.0] 122 | - The KDoc formatter now participates in regular IDE source code 123 | formatting (e.g. Code > Reformat Code). This can be turned off via a 124 | setting. 125 | - The markup conversion (if enabled) now converts [] and {@linkplain} 126 | tags to the KDoc equivalent. 127 | - Fix bug where preformatted text immediately following a TODO comment 128 | would be joined into the TODO. 129 | 130 | ## [1.3.3] 131 | - Don't break lines inside link text which will include the comment 132 | asterisk 133 | - Allow formatting during indexing 134 | 135 | ## [1.3.2] 136 | - Bugfixes and update deprecated API usage for 2021.2 compatibility 137 | 138 | ## [1.3.1] 139 | - Fixes a few bugs around markup conversion not producing the right 140 | number of blank lines for a

, and adds a few more prefixes as 141 | non-breakable (e.g. if you have an em dash in your sentence -- like 142 | this -- we don't want the "--" to be placed at the beginning of a 143 | line.) 144 | - Adds an --add-punctuation command line flag and IDE setting to 145 | optionally add closing periods on capitalized paragraphs at the end of 146 | the comment. 147 | - Special cases TODO: comments (placing them in a block by themselves 148 | and using hanging indents). 149 | 150 | ## [1.3.0] 151 | - Many improves to the markdown handling, such as quoted blocks, 152 | headers, list continuations, etc. 153 | - Markup conversion, which until this point could convert inline tags 154 | such as **bold** into **bold**, etc, now handles many block level tags 155 | too, such as \

, \

, etc. 156 | - The IDE plugin can now also reformat line comments under the caret. 157 | (This is opt-in via options.) 158 | 159 | ## [1.2.0] 160 | - IDE settings panel 161 | - Ability to alternate formatting between greedy and optimal line 162 | breaking when invoked repeatedly (and for short comments, alternating 163 | between single line and multiple lines.) 164 | 165 | ## [1.1.2] 166 | - Basic support for .editorconfig files. 167 | 168 | ## [1.1.0] 169 | - Support for setting maximum comment width (capped by the maximum line 170 | width). 171 | 172 | ## [1.0.0] 173 | - Initial version 174 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/GitRangeFilter.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import java.io.BufferedReader 20 | import java.io.File 21 | import java.io.InputStreamReader 22 | import java.nio.file.Files 23 | 24 | /** 25 | * Searches the current git commit for modified regions and returns these. 26 | * 27 | * TODO: Allow specifying an arbitrary range of git sha's. This requires some work to figure out the 28 | * correct line numbers in the current version from older patches. 29 | */ 30 | class GitRangeFilter private constructor(rangeMap: RangeMap) : LineRangeFilter(rangeMap) { 31 | companion object { 32 | fun create(gitPath: String, fileInRepository: File, staged: Boolean = false): GitRangeFilter? { 33 | val git = findGit(gitPath) ?: return null 34 | val args = mutableListOf() 35 | val gitRepo = findGitRepo(fileInRepository) ?: return null 36 | val output = Files.createTempFile("gitshow", ".diff").toFile() 37 | args.add(git.path) 38 | args.add("--git-dir=$gitRepo") 39 | args.add("--no-pager") 40 | if (staged) { 41 | args.add("diff") 42 | args.add("--cached") 43 | } else { 44 | args.add("show") 45 | } 46 | args.add("--no-color") 47 | args.add("--no-prefix") 48 | args.add("--unified=0") 49 | args.add("--output=$output") 50 | if (!executeProcess(args)) { 51 | return null 52 | } 53 | val root = gitRepo.parentFile 54 | val diff = output.readText() 55 | return create(root, diff) 56 | } 57 | 58 | /** 59 | * Creates range from the given diff contents. Extracted to be separate from the git invocation 60 | * above for unit test purposes. 61 | */ 62 | fun create(root: File?, diff: String): GitRangeFilter { 63 | val rangeMap = RangeMap() 64 | var currentPath = root 65 | for (line in diff.split("\n")) { 66 | if (line.startsWith("+++ ")) { 67 | val relative = line.substring(4) 68 | // Canonicalize files here to match the canonicalization we perform in 69 | // KDocFileFormattingOptions.parse (which is necessary such that we don't 70 | // accidentally handle relative paths like "./" etc as "foo/./bar" which 71 | // isn't treated as equal to "foo/bar"). 72 | currentPath = (if (root != null) File(root, relative) else File(relative)).canonicalFile 73 | } else if (line.startsWith("@@ ")) { 74 | //noinspection FileComparisons 75 | if (currentPath === root || currentPath == null || !currentPath.path.endsWith(".kt")) { 76 | continue 77 | } 78 | val rangeStart = line.indexOf('+') + 1 79 | val rangeEnd = line.indexOf(' ', rangeStart + 1) 80 | val range = line.substring(rangeStart, rangeEnd) 81 | val lineCountStart = range.indexOf(",") 82 | val startLine: Int 83 | val lineCount: Int 84 | if (lineCountStart == -1) { 85 | startLine = range.toInt() 86 | lineCount = 1 87 | } else { 88 | startLine = range.substring(0, lineCountStart).toInt() 89 | lineCount = range.substring(lineCountStart + 1).toInt() 90 | } 91 | rangeMap.addRange(currentPath, startLine, startLine + lineCount) 92 | } 93 | } 94 | 95 | return GitRangeFilter(rangeMap) 96 | } 97 | 98 | private fun executeProcess(args: List): Boolean { 99 | try { 100 | val process = Runtime.getRuntime().exec(args.toTypedArray()) 101 | val input = BufferedReader(InputStreamReader(process.inputStream)) 102 | val error = BufferedReader(InputStreamReader(process.errorStream)) 103 | val exitVal = process.waitFor() 104 | if (exitVal != 0) { 105 | val sb = StringBuilder() 106 | sb.append("Failed to run git command.\n") 107 | sb.append("Command args:\n") 108 | for (arg in args) { 109 | sb.append(" ").append(arg).append("\n") 110 | } 111 | sb.append("Standard output:\n") 112 | var line: String? 113 | while (input.readLine().also { line = it } != null) { 114 | sb.append(line).append("\n") 115 | } 116 | sb.append("Error output:\n") 117 | while (error.readLine().also { line = it } != null) { 118 | sb.append(line).append("\n") 119 | } 120 | input.close() 121 | error.close() 122 | System.err.println(sb.toString()) 123 | return false 124 | } 125 | return true 126 | } catch (t: Throwable) { 127 | val sb = StringBuilder() 128 | for (arg in args) { 129 | sb.append(" ").append(arg).append("\n") 130 | } 131 | System.err.println(sb.toString()) 132 | return false 133 | } 134 | } 135 | 136 | /** Returns the .git folder for the [file] or directory somewhere in the report. */ 137 | private fun findGitRepo(file: File): File? { 138 | var curr = file.absoluteFile 139 | while (true) { 140 | val git = File(curr, ".git") 141 | if (git.isDirectory) { 142 | return git 143 | } 144 | curr = curr.parentFile ?: return null 145 | } 146 | } 147 | 148 | private fun findGit(gitPath: String): File? { 149 | if (gitPath.isNotEmpty()) { 150 | val file = File(gitPath) 151 | return if (file.exists()) { 152 | file 153 | } else { 154 | System.err.println("$gitPath does not exist") 155 | null 156 | } 157 | } 158 | 159 | val git = findOnPath("git") ?: findOnPath("git.exe") 160 | if (git != null) { 161 | val gitFile = File(git) 162 | if (!gitFile.canExecute()) { 163 | System.err.println("Cannot execute $gitFile") 164 | return null 165 | } 166 | return gitFile 167 | } else { 168 | return null 169 | } 170 | } 171 | 172 | private fun findOnPath(target: String): String? { 173 | val path = System.getenv("PATH")?.split(File.pathSeparator) ?: return null 174 | for (binDir in path) { 175 | val file = File(binDir + File.separator + target) 176 | if (file.isFile) { // maybe file.canExecute() too but not sure how .bat files behave 177 | return file.path 178 | } 179 | } 180 | return null 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/facebook/ktfmt/kdoc/UtilitiesTest.kt: -------------------------------------------------------------------------------- 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 com.google.common.truth.Truth.assertThat 20 | import org.junit.Test 21 | import org.junit.runner.RunWith 22 | import org.junit.runners.JUnit4 23 | 24 | @RunWith(JUnit4::class) 25 | class UtilitiesTest { 26 | @Test 27 | fun testFindSamePosition() { 28 | fun check(newWithCaret: String, oldWithCaret: String) { 29 | val oldCaretIndex = oldWithCaret.indexOf('|') 30 | val newCaretIndex = newWithCaret.indexOf('|') 31 | assertThat(oldCaretIndex != -1).isTrue() 32 | assertThat(newCaretIndex != -1).isTrue() 33 | val old = oldWithCaret.substring(0, oldCaretIndex) + oldWithCaret.substring(oldCaretIndex + 1) 34 | val new = newWithCaret.substring(0, newCaretIndex) + newWithCaret.substring(newCaretIndex + 1) 35 | val newPos = findSamePosition(old, oldCaretIndex, new) 36 | 37 | val actual = new.substring(0, newPos) + "|" + new.substring(newPos) 38 | assertThat(actual).isEqualTo(newWithCaret) 39 | } 40 | 41 | // Prefix match 42 | check("|/** Test\n Different Middle End */", "|/** Test2 End */") 43 | check("/|** Test\n Different Middle End */", "/|** Test2 End */") 44 | check("/*|* Test\n Different Middle End */", "/*|* Test2 End */") 45 | check("/**| Test\n Different Middle End */", "/**| Test2 End */") 46 | check("/** |Test\n Different Middle End */", "/** |Test2 End */") 47 | check("/** T|est\n Different Middle End */", "/** T|est2 End */") 48 | check("/** Te|st\n Different Middle End */", "/** Te|st2 End */") 49 | check("/** Tes|t\n Different Middle End */", "/** Tes|t2 End */") 50 | check("/** Test|\n Different Middle End */", "/** Test|2 End */") 51 | // End match 52 | check("/** Test\n Different Middle| End */", "/** Test2| End */") 53 | check("/** Test\n Different Middle E|nd */", "/** Test2 E|nd */") 54 | check("/** Test\n Different Middle En|d */", "/** Test2 En|d */") 55 | check("/** Test\n Different Middle End| */", "/** Test2 End| */") 56 | check("/** Test\n Different Middle End |*/", "/** Test2 End |*/") 57 | check("/** Test\n Different Middle End *|/", "/** Test2 End *|/") 58 | check("/** Test\n Different Middle End */|", "/** Test2 End */|") 59 | 60 | check("|/**\nTest End\n*/", "|/** Test End */") 61 | check("/|**\nTest End\n*/", "/|** Test End */") 62 | check("/*|*\nTest End\n*/", "/*|* Test End */") 63 | check("/**|\nTest End\n*/", "/**| Test End */") 64 | check("/**\n|Test End\n*/", "/** |Test End */") 65 | check("/**\nT|est End\n*/", "/** T|est End */") 66 | check("/**\nTe|st End\n*/", "/** Te|st End */") 67 | check("/**\nTes|t End\n*/", "/** Tes|t End */") 68 | check("/**\nTest| End\n*/", "/** Test| End */") 69 | check("/**\nTest |End\n*/", "/** Test |End */") 70 | check("/**\nTest E|nd\n*/", "/** Test E|nd */") 71 | check("/**\nTest En|d\n*/", "/** Test En|d */") 72 | check("/**\nTest End|\n*/", "/** Test End| */") 73 | check("/**\nTest End\n|*/", "/** Test End |*/") 74 | check("/**\nTest End\n*|/", "/** Test End *|/") 75 | check("/**\nTest End\n*/|", "/** Test End */|") 76 | 77 | check("|/** Test End */", "|/** Test2 End */") 78 | check("/|** Test End */", "/|** Test2 End */") 79 | check("/*|* Test End */", "/*|* Test2 End */") 80 | check("/**| Test End */", "/**| Test2 End */") 81 | check("/** |Test End */", "/** |Test2 End */") 82 | check("/** T|est End */", "/** T|est2 End */") 83 | check("/** Te|st End */", "/** Te|st2 End */") 84 | check("/** Tes|t End */", "/** Tes|t2 End */") 85 | check("/** Test| End */", "/** Test|2 End */") 86 | check("/** Test |End */", "/** Test2 |End */") 87 | check("/** Test E|nd */", "/** Test2 E|nd */") 88 | check("/** Test En|d */", "/** Test2 En|d */") 89 | check("/** Test End| */", "/** Test2 End| */") 90 | check("/** Test End |*/", "/** Test2 End |*/") 91 | check("/** Test End *|/", "/** Test2 End *|/") 92 | check("/** Test End */|", "/** Test2 End */|") 93 | } 94 | 95 | @Test 96 | fun testGetParamName() { 97 | assertThat("@param foo".getParamName()).isEqualTo("foo") 98 | assertThat("@param foo bar".getParamName()).isEqualTo("foo") 99 | assertThat("@param foo;".getParamName()).isEqualTo("foo") 100 | assertThat(" \t@param\t foo bar.".getParamName()).isEqualTo("foo") 101 | assertThat("@param[foo]".getParamName()).isEqualTo("foo") 102 | assertThat("@param [foo]".getParamName()).isEqualTo("foo") 103 | assertThat("@param ".getParamName()).isNull() 104 | } 105 | 106 | @Test 107 | fun testComputeWords() { 108 | fun List.describe(): String { 109 | return "listOf(${this.joinToString(", ") { "\"$it\"" }})" 110 | } 111 | fun check(text: String, expected: List, customizeParagraph: (Paragraph) -> Unit = {}) { 112 | val task = FormattingTask(KDocFormattingOptions(12), "/** $text */", "") 113 | val paragraph = Paragraph(task) 114 | paragraph.content.append(text) 115 | customizeParagraph(paragraph) 116 | val words = paragraph.computeWords() 117 | 118 | assertThat(words.describe()).isEqualTo(expected.describe()) 119 | } 120 | check("Foo", listOf("Foo")) 121 | check("Foo Bar Baz", listOf("Foo", "Bar", "Baz")) 122 | check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.quoted = true } 123 | check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.hanging = true } 124 | check("1. Foo", listOf("1.", "Foo")) 125 | // "1." can't start a word; if it ends up at the beginning of a line it becomes 126 | // a numbered element. 127 | check("Foo 1.", listOf("Foo 1.")) 128 | check("Foo bar [Link Text] foo bar.", listOf("Foo", "bar", "[Link Text]", "foo", "bar.")) 129 | check("Interval [0, 1) foo bar.", listOf("Interval [0, 1)", "foo", "bar.")) 130 | 131 | // ">" cannot start a word; it would become quoted text 132 | check("if >= 3", listOf("if >=", "3")) 133 | check("if >= 3.", listOf("if >= 3.")) 134 | 135 | check( 136 | "SDK version - [`Partial(Mode.UseIfAvailable)`](Partial) on API 24+", 137 | listOf("SDK", "version - [`Partial(Mode.UseIfAvailable)`](Partial)", "on", "API", "24+")) 138 | 139 | check( 140 | "Z orders can range from Integer.MIN_VALUE to Integer.MAX_VALUE. Default z order " + 141 | " index is 0. [SurfaceControlWrapper] instances are positioned back-to-front.", 142 | listOf( 143 | "Z", 144 | "orders", 145 | "can", 146 | "range", 147 | "from", 148 | "Integer.MIN_VALUE", 149 | "to", 150 | "Integer.MAX_VALUE.", 151 | "Default", 152 | "z", 153 | "order", 154 | "index", 155 | "is 0. [SurfaceControlWrapper]", 156 | "instances", 157 | "are", 158 | "positioned", 159 | "back-to-front.")) 160 | check( 161 | "Equates to `cmd package compile -f -m speed ` on API 24+.", 162 | listOf( 163 | "Equates", 164 | "to", 165 | "`cmd", 166 | "package", 167 | "compile", 168 | "-f", 169 | "-m", 170 | "speed", 171 | "`", 172 | "on", 173 | "API", 174 | "24+.")) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /library/src/test/kotlin/com/facebook/ktfmt/kdoc/DokkaVerifier.kt: -------------------------------------------------------------------------------- 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 | @file:Suppress("PropertyName", "PrivatePropertyName") 18 | 19 | package com.facebook.ktfmt.kdoc 20 | 21 | import com.google.common.truth.Truth.assertThat 22 | import java.io.BufferedReader 23 | import java.io.File 24 | 25 | /** 26 | * Verifies that two KDoc comment strings render to the same HTML documentation using Dokka. This is 27 | * used by the test infrastructure to make sure that the transformations we're allowing are not 28 | * changing the appearance of the documentation. 29 | * 30 | * Unfortunately, just diffing HTML strings isn't always enough, because dokka will preserve some 31 | * text formatting which is immaterial to the HTML appearance. Therefore, if you've also installed 32 | * Pandoc, it will use that to generate a text rendering of the HTML which is then used for diffing 33 | * instead. (Even this isn't fullproof because pandoc also preserves some details that should not 34 | * matter). Text rendering does drop a lot of markup (such as bold and italics) so it would be 35 | * better to compare in some other format, such as PDF, but unfortunately, the PDF rendering doesn't 36 | * appear to be stable; rendering the same document twice yields a binary diff. 37 | * 38 | * Dokka no longer provides a fat/shadow jar; instead you have to download a bunch of different 39 | * dependencies. Therefore, for convenience this is set up to point to an AndroidX checkout, which 40 | * has all the prebuilts. Point the below to AndroidX and the rest should work. 41 | */ 42 | class DokkaVerifier(private val tempFolder: File) { 43 | // Configuration parameters 44 | // Checkout of https://github.com/androidx/androidx 45 | private val ANDROIDX_HOME: String? = null 46 | 47 | // Optional install of pandoc, e.g. "/opt/homebrew/bin/pandoc" 48 | private val PANDOC: String? = null 49 | 50 | // JDK install 51 | private val JAVA_HOME: String? = System.getenv("JAVA_HOME") ?: System.getProperty("java.home") 52 | 53 | fun verify(before: String, after: String) { 54 | JAVA_HOME ?: return 55 | ANDROIDX_HOME ?: return 56 | 57 | val androidx = File(ANDROIDX_HOME) 58 | if (!androidx.isDirectory) { 59 | return 60 | } 61 | 62 | val prebuilts = File(androidx, "prebuilts") 63 | if (!prebuilts.isDirectory) { 64 | println("AndroidX prebuilts not found; not verifying with Dokka") 65 | } 66 | val cli = find(prebuilts, "org.jetbrains.dokka", "dokka-cli") 67 | val analysis = find(prebuilts, "org.jetbrains.dokka", "dokka-analysis") 68 | val base = find(prebuilts, "org.jetbrains.dokka", "dokka-base") 69 | val compiler = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-compiler") 70 | val intellij = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-intellij") 71 | val coroutines = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-coroutines-core") 72 | val html = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-html-jvm") 73 | val freemarker = find(prebuilts, "org.freemarker", "freemarker") 74 | 75 | val src = File(tempFolder, "src") 76 | val out = File(tempFolder, "dokka") 77 | src.mkdirs() 78 | out.mkdirs() 79 | 80 | val beforeFile = File(src, "before.kt") 81 | beforeFile.writeText("${before.split("\n").joinToString("\n") { it.trim() }}\nclass Before\n") 82 | 83 | val afterFile = File(src, "after.kt") 84 | afterFile.writeText("${after.split("\n").joinToString("\n") { it.trim() }}\nclass After\n") 85 | 86 | val args = mutableListOf() 87 | args.add(File(JAVA_HOME, "bin/java").path) 88 | args.add("-jar") 89 | args.add(cli.path) 90 | args.add("-pluginsClasspath") 91 | val pathSeparator = 92 | ";" // instead of File.pathSeparator as would have been reasonable (e.g. : on Unix) 93 | val path = 94 | listOf(analysis, base, compiler, intellij, coroutines, html, freemarker).joinToString( 95 | pathSeparator) { 96 | it.path 97 | } 98 | args.add(path) 99 | args.add("-sourceSet") 100 | args.add("-src $src") // (nested parameter within -sourceSet) 101 | args.add("-outputDir") 102 | args.add(out.path) 103 | executeProcess(args) 104 | 105 | fun getHtml(file: File): String { 106 | val rendered = file.readText() 107 | val begin = rendered.indexOf("
") 108 | val end = rendered.indexOf("
", begin) 109 | return rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n") 110 | } 111 | 112 | fun getText(file: File): String? { 113 | return if (PANDOC != null) { 114 | val pandocFile = File(PANDOC) 115 | if (!pandocFile.isFile) { 116 | error("Cannot execute $pandocFile") 117 | } 118 | val outFile = File(out, "text.text") 119 | executeProcess(listOf(PANDOC, file.path, "-o", outFile.path)) 120 | val rendered = outFile.readText() 121 | 122 | val begin = rendered.indexOf("[]{.copy-popup-icon}Content copied to clipboard") 123 | val end = rendered.indexOf("::: tabbedcontent", begin) 124 | rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n") 125 | } else { 126 | null 127 | } 128 | } 129 | 130 | val indexBefore = File("$out/root/[root]/-before/index.html") 131 | val beforeContents = getHtml(indexBefore) 132 | val indexAfter = File("$out/root/[root]/-after/index.html") 133 | val afterContents = getHtml(indexAfter) 134 | if (beforeContents != afterContents) { 135 | val beforeText = getText(indexBefore) 136 | val afterText = getText(indexAfter) 137 | if (beforeText != null && afterText != null) { 138 | assertThat(beforeText).isEqualTo(afterText) 139 | return 140 | } 141 | 142 | assertThat(beforeContents).isEqualTo(afterContents) 143 | } 144 | } 145 | 146 | private fun find(prebuilts: File, group: String, artifact: String): File { 147 | val versionDir = File(prebuilts, "androidx/external/${group.replace('.','/')}/$artifact") 148 | val versions = 149 | versionDir.listFiles().filter { it.name.first().isDigit() }.sortedByDescending { it.name } 150 | for (version in versions.map { it.name }) { 151 | val jar = File(versionDir, "$version/$artifact-$version.jar") 152 | if (jar.isFile) { 153 | return jar 154 | } 155 | } 156 | error("Could not find a valid jar file for $group:$artifact") 157 | } 158 | 159 | private fun executeProcess(args: List) { 160 | var input: BufferedReader? = null 161 | var error: BufferedReader? = null 162 | try { 163 | val process = Runtime.getRuntime().exec(args.toTypedArray()) 164 | input = process.inputStream.bufferedReader() 165 | error = process.errorStream.bufferedReader() 166 | val exitVal = process.waitFor() 167 | if (exitVal != 0) { 168 | val sb = StringBuilder() 169 | sb.append("Failed to execute process\n") 170 | sb.append("Command args:\n") 171 | for (arg in args) { 172 | sb.append(" ").append(arg).append("\n") 173 | } 174 | sb.append("Standard output:\n") 175 | var line: String? 176 | while (input.readLine().also { line = it } != null) { 177 | sb.append(line).append("\n") 178 | } 179 | sb.append("Error output:\n") 180 | while (error.readLine().also { line = it } != null) { 181 | sb.append(line).append("\n") 182 | } 183 | error(sb.toString()) 184 | } 185 | } catch (t: Throwable) { 186 | val sb = StringBuilder() 187 | for (arg in args) { 188 | sb.append(" ").append(arg).append("\n") 189 | } 190 | t.printStackTrace() 191 | error("Could not run process:\n$sb") 192 | } finally { 193 | input?.close() 194 | error?.close() 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/Table.kt: -------------------------------------------------------------------------------- 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 kotlin.math.max 20 | 21 | class Table( 22 | private val columns: Int, 23 | private val widths: List, 24 | private val rows: List, 25 | private val align: List, 26 | private val original: List 27 | ) { 28 | fun original(): List { 29 | return original 30 | } 31 | 32 | /** 33 | * Format the table. Note that table rows cannot be broken into multiple lines in Markdown tables, 34 | * so the [maxWidth] here is used to decide whether to add padding around the table only, and it's 35 | * quite possible for the table to format to wider lengths than [maxWidth]. 36 | */ 37 | fun format(maxWidth: Int = Integer.MAX_VALUE): List { 38 | val tableMaxWidth = 39 | 2 + widths.sumOf { it + 2 } // +2: "| " in each cell and final " |" on the right 40 | 41 | val pad = tableMaxWidth <= maxWidth 42 | val lines = mutableListOf() 43 | for (i in rows.indices) { 44 | val sb = StringBuilder() 45 | val row = rows[i] 46 | for (column in 0 until row.cells.size) { 47 | sb.append('|') 48 | if (pad) { 49 | sb.append(' ') 50 | } 51 | val cell = row.cells[column] 52 | val width = widths[column] 53 | val s = 54 | if (align[column] == Align.CENTER && i > 0) { 55 | String.format( 56 | "%-${width}s", 57 | String.format("%${cell.length + (width - cell.length) / 2}s", cell)) 58 | } else if (align[column] == Align.RIGHT && i > 0) { 59 | String.format("%${width}s", cell) 60 | } else { 61 | String.format("%-${width}s", cell) 62 | } 63 | sb.append(s) 64 | if (pad) { 65 | sb.append(' ') 66 | } 67 | } 68 | sb.append('|') 69 | lines.add(sb.toString()) 70 | sb.clear() 71 | 72 | if (i == 0) { 73 | for (column in 0 until row.cells.size) { 74 | sb.append('|') 75 | var width = widths[column] 76 | if (align[column] != Align.LEFT) { 77 | width-- 78 | if (align[column] == Align.CENTER) { 79 | sb.append(':') 80 | width-- 81 | } 82 | } 83 | if (pad) { 84 | sb.append('-') 85 | } 86 | val s = "-".repeat(width) 87 | sb.append(s) 88 | if (pad) { 89 | sb.append('-') 90 | } 91 | if (align[column] != Align.LEFT) { 92 | sb.append(':') 93 | } 94 | } 95 | sb.append('|') 96 | lines.add(sb.toString()) 97 | sb.clear() 98 | } 99 | } 100 | 101 | return lines 102 | } 103 | 104 | companion object { 105 | /** 106 | * If the line starting at index [start] begins a table, return that table as well as the index 107 | * of the first line after the table. 108 | */ 109 | fun getTable( 110 | lines: List, 111 | start: Int, 112 | lineContent: (String) -> String 113 | ): Pair? { 114 | if (start > lines.size - 2) { 115 | return null 116 | } 117 | val headerLine = lineContent(lines[start]) 118 | val separatorLine = lineContent(lines[start + 1]) 119 | val barCount = countSeparators(headerLine) 120 | if (!isHeaderDivider(barCount, separatorLine.trim())) { 121 | return null 122 | } 123 | val header = getRow(headerLine) ?: return null 124 | val rows = mutableListOf() 125 | rows.add(header) 126 | 127 | val dividerRow = getRow(separatorLine) ?: return null 128 | 129 | var i = start + 2 130 | while (i < lines.size) { 131 | val line = lineContent(lines[i]) 132 | if (!line.contains("|")) { 133 | break 134 | } 135 | val row = getRow(line) ?: break 136 | rows.add(row) 137 | i++ 138 | } 139 | 140 | val rowsAndDivider = rows + dividerRow 141 | if (rowsAndDivider.all { 142 | val first = it.cells.firstOrNull() 143 | first != null && first.isBlank() 144 | }) { 145 | rowsAndDivider.forEach { if (it.cells.isNotEmpty()) it.cells.removeAt(0) } 146 | } 147 | 148 | // val columns = rows.maxOf { it.cells.size } 149 | val columns = dividerRow.cells.size 150 | val maxColumns = rows.maxOf { it.cells.size } 151 | val widths = mutableListOf() 152 | for (column in 0 until maxColumns) { 153 | widths.add(3) 154 | } 155 | for (row in rows) { 156 | for (column in 0 until row.cells.size) { 157 | widths[column] = max(widths[column], row.cells[column].length) 158 | } 159 | for (column in row.cells.size until columns) { 160 | row.cells.add("") 161 | } 162 | } 163 | 164 | val align = mutableListOf() 165 | for (cell in dividerRow.cells) { 166 | val direction = 167 | if (cell.endsWith(":")) { 168 | if (cell.startsWith(":-")) { 169 | Align.CENTER 170 | } else { 171 | Align.RIGHT 172 | } 173 | } else { 174 | Align.LEFT 175 | } 176 | align.add(direction) 177 | } 178 | for (column in align.size until maxColumns) { 179 | align.add(Align.LEFT) 180 | } 181 | val table = 182 | Table(columns, widths, rows, align, lines.subList(start, i).map { lineContent(it) }) 183 | return Pair(table, i) 184 | } 185 | 186 | /** Returns true if the given String looks like a markdown table header divider. */ 187 | private fun isHeaderDivider(barCount: Int, s: String): Boolean { 188 | var i = 0 189 | var count = 0 190 | while (i < s.length) { 191 | val c = s[i++] 192 | if (c == '\\') { 193 | i++ 194 | } else if (c == '|') { 195 | count++ 196 | } else if (c.isWhitespace() || c == ':') { 197 | continue 198 | } else if (c == '-' && 199 | (s.startsWith("--", i) || 200 | s.startsWith("-:", i) || 201 | i > 1 && s.startsWith(":-:", i - 2) || 202 | i > 1 && s.startsWith(":--", i - 2))) { 203 | while (i < s.length && s[i] == '-') { 204 | i++ 205 | } 206 | } else { 207 | return false 208 | } 209 | } 210 | 211 | return barCount == count 212 | } 213 | 214 | private fun getRow(s: String): Row? { 215 | // Can't just use String.split('|') because that would not handle escaped |'s 216 | if (s.indexOf('|') == -1) { 217 | return null 218 | } 219 | val row = Row() 220 | var i = 0 221 | var end = 0 222 | while (end < s.length) { 223 | val c = s[end] 224 | if (c == '\\') { 225 | end++ 226 | } else if (c == '|') { 227 | val cell = s.substring(i, end).trim() 228 | if (end > 0) { 229 | row.cells.add(cell.trim()) 230 | } 231 | i = end + 1 232 | } 233 | end++ 234 | } 235 | if (end > i) { 236 | val cell = s.substring(i, end).trim() 237 | if (cell.isNotEmpty()) { 238 | row.cells.add(cell.trim()) 239 | } 240 | } 241 | 242 | return row 243 | } 244 | 245 | private fun countSeparators(s: String): Int { 246 | var i = 0 247 | var count = 0 248 | while (i < s.length) { 249 | val c = s[i] 250 | if (c == '|') { 251 | count++ 252 | } else if (c == '\\') { 253 | i++ 254 | } 255 | i++ 256 | } 257 | return count 258 | } 259 | } 260 | 261 | enum class Align { 262 | LEFT, 263 | RIGHT, 264 | CENTER 265 | } 266 | 267 | class Row { 268 | val cells = mutableListOf() 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/EditorConfigs.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import com.facebook.ktfmt.kdoc.KDocFormattingOptions 20 | import java.io.File 21 | import java.util.* 22 | 23 | /** 24 | * Basic support for [.editorconfig] files (http://https://editorconfig.org/). 25 | * 26 | * This will construct [KDocFormattingOptions] applicable for a given file. The 27 | * [KDocFormattingOptions.maxLineWidth] property is initialized from the [.editorconfig] property 28 | * `max_line_length` applicable to Kotlin source files, and the 29 | * [KDocFormattingOptions.maxCommentWidth] is initialized from the property `max_line_length` 30 | * applicable to Markdown source files (but **not** inherited from non-Markdown specific 31 | * declarations such as [*].] 32 | * 33 | * We're processing it ourselves here instead of using one of the available editorconfig libraries 34 | * on GitHub because of that special handling of markdown settings where we want to consult values 35 | * without inheritance. 36 | */ 37 | object EditorConfigs { 38 | var root: KDocFormattingOptions? = null 39 | set(value) { 40 | dirToConfig.clear() 41 | field = value 42 | } 43 | 44 | private val dirToConfig = mutableMapOf() 45 | 46 | fun getOptions(file: File): KDocFormattingOptions { 47 | val parent = file.parentFile ?: return root ?: KDocFormattingOptions() 48 | return getConfig(parent)?.getOptions() ?: root ?: KDocFormattingOptions() 49 | } 50 | 51 | @Suppress("FileComparisons") 52 | private fun getConfig(dir: File?): EditorConfig? { 53 | dir ?: return null 54 | 55 | val existing = dirToConfig[dir] 56 | if (existing != null) { 57 | return if (existing !== EditorConfig.NONE) existing else null 58 | } 59 | 60 | val configFile = 61 | findEditorConfigFile(dir) 62 | ?: run { 63 | dirToConfig[dir] = EditorConfig.NONE 64 | 65 | var curr = dir.parentFile 66 | while (true) { 67 | dirToConfig[curr] = EditorConfig.NONE 68 | curr = curr.parentFile ?: break 69 | } 70 | 71 | return null 72 | } 73 | 74 | val configFolder = configFile.parentFile 75 | val parentConfigFolder = configFolder?.parentFile 76 | val parent = getConfig(parentConfigFolder) 77 | val config = EditorConfig.createEditorConfig(configFile, parent) 78 | dirToConfig[dir] = config 79 | 80 | if (configFolder != dir) { 81 | var curr: File = dir 82 | while (true) { 83 | if (curr == configFolder) { 84 | break 85 | } else { 86 | dirToConfig[curr] = config 87 | } 88 | curr = curr.parentFile ?: break 89 | } 90 | } 91 | 92 | return config 93 | } 94 | 95 | private fun findEditorConfigFile(fromDir: File): File? { 96 | var dir = fromDir 97 | while (true) { 98 | val file = File(dir, ".editorconfig") 99 | if (file.isFile) { 100 | return file 101 | } 102 | dir = dir.parentFile ?: return null 103 | } 104 | } 105 | 106 | class EditorConfig 107 | private constructor( 108 | private val root: Boolean, 109 | private val file: File, 110 | private val parent: EditorConfig?, 111 | private val sections: List 112 | ) { 113 | private data class SectionMap(val section: String, val map: Map) 114 | 115 | private var options: KDocFormattingOptions? = null 116 | 117 | fun getOptions(): KDocFormattingOptions { 118 | return options ?: computeOptions().also { options = it } 119 | } 120 | 121 | fun getValue(key: String, eligibleSection: String, includeRoot: Boolean = true): Any? { 122 | var value: String? = null 123 | for (section in sections) { 124 | val name = section.section 125 | if (includeRoot && name == "[*]" || name.contains(eligibleSection)) { 126 | // last applicable value wins 127 | section.map[key]?.let { value = it } 128 | } 129 | } 130 | 131 | if (value == null && !root) { 132 | return parent?.getValue(key, eligibleSection, includeRoot) 133 | } 134 | 135 | return value 136 | } 137 | 138 | private fun computeOptions(): KDocFormattingOptions { 139 | val options = 140 | (if (!root) parent?.getOptions()?.copy() else null) 141 | ?: EditorConfigs.root?.copy() 142 | ?: KDocFormattingOptions() 143 | 144 | getValue("max_line_length", "*.kt")?.let { stringValue -> 145 | if (stringValue == "unset") { 146 | EditorConfigs.root?.maxLineWidth?.let { options.maxLineWidth = it } 147 | } else { 148 | (stringValue as? String)?.toIntOrNull()?.let { value -> options.maxLineWidth = value } 149 | } 150 | } 151 | 152 | getValue("max_line_length", "*.md", false)?.let { stringValue -> 153 | if (stringValue == "unset") { 154 | EditorConfigs.root?.maxCommentWidth?.let { options.maxCommentWidth = it } 155 | } else { 156 | (stringValue as? String)?.toIntOrNull()?.let { value -> options.maxCommentWidth = value } 157 | } 158 | } 159 | 160 | getValue("indent_size", "*.kt")?.let { stringValue -> 161 | if (stringValue == "unset") { 162 | EditorConfigs.root?.hangingIndent?.let { options.hangingIndent = it } 163 | } else { 164 | (stringValue as? String)?.toIntOrNull()?.let { value -> options.hangingIndent = value } 165 | } 166 | } 167 | 168 | getValue("tab_width", "*.kt")?.let { stringValue -> 169 | if (stringValue == "unset") { 170 | EditorConfigs.root?.tabWidth?.let { options.tabWidth = it } 171 | } else { 172 | (stringValue as? String)?.toIntOrNull()?.let { value -> options.tabWidth = value } 173 | } 174 | } 175 | 176 | val oneline = 177 | getValue("kdoc_formatter_doc_do_not_wrap_if_one_line", "*.kt") 178 | ?: getValue("ij_kotlin_doc_do_not_wrap_if_one_line", "*.kt") 179 | ?: getValue("ij_java_doc_do_not_wrap_if_one_line", "*.java") 180 | oneline?.let { stringValue -> 181 | if (stringValue == "unset") { 182 | EditorConfigs.root?.collapseSingleLine?.let { options.collapseSingleLine = it } 183 | } else { 184 | (stringValue as? String)?.toBoolean()?.let { value -> 185 | options.collapseSingleLine = !value 186 | } 187 | } 188 | } 189 | 190 | return options 191 | } 192 | 193 | override fun toString(): String { 194 | return file.toString() 195 | } 196 | 197 | companion object { 198 | val NONE = EditorConfig(true, File(""), null, emptyList()) 199 | 200 | // If root -- no need to keep going 201 | fun createEditorConfig(config: File, parent: EditorConfig?): EditorConfig { 202 | // Maybe have a default flag, e.g. --default-line-width 203 | val sections = ArrayList() 204 | var section: SectionMap? = null 205 | var map = HashMap() 206 | var root = false 207 | 208 | val lines = config.readLines() 209 | for (line in lines) { 210 | if (line.startsWith("#") || line.startsWith(";") || line.isBlank()) { 211 | continue 212 | } 213 | if (line.startsWith("[")) { 214 | val globs = 215 | line 216 | .removePrefix("[") 217 | .removeSuffix("]") 218 | .removePrefix("{") 219 | .removeSuffix("}") 220 | .split(",") 221 | 222 | if (globs.any { 223 | it == "*" || it == "*.kt" || it == "*.kts" || it == "*.md" || it == "*.java" 224 | }) { 225 | map = HashMap() 226 | section = SectionMap(line, map) 227 | sections.add(section) 228 | } else { 229 | section = null 230 | } 231 | } else { 232 | val eq = line.indexOf('=') 233 | 234 | val key = line.substring(0, eq).trim().lowercase() 235 | if (key == "root") { 236 | root = line.substring(eq + 1).trim().toBoolean() 237 | } else 238 | when (key) { 239 | "max_line_length", 240 | "indent_size", 241 | "tab_width", 242 | "kdoc_formatter_doc_do_not_wrap_if_one_line", 243 | "ij_java_doc_do_not_wrap_if_one_line" -> 244 | if (section != null) { 245 | val value = line.substring(eq + 1).trim() 246 | map[key] = value 247 | } 248 | } 249 | } 250 | } 251 | 252 | return EditorConfig(root, config, parent, sections) 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /gradle-plugin/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KDoc Formatter 2 | ============== 3 | 4 | Reformats Kotlin KDoc comments, reflowing text and other cleanup, both 5 | via IDE plugin and command line utility. 6 | 7 | This tool reflows comments in KDoc; either on a file or recursively over 8 | nested folders, as well as an IntelliJ IDE plugin where you can reflow 9 | the current comment around the cursor. 10 | 11 | Here's an example of the plugin in use, showing editing a comment and 12 | then applying the formatting action to clean it up: 13 | 14 | ![Screenshot](cleanup.gif) 15 | 16 | In addition to general cleanup, this is also handy when you're editing a 17 | comment and you need to reflow the paragraph because the current line is 18 | too long or too short: 19 | 20 | ![Screenshot](modify-line.gif) 21 | 22 | Features 23 | -------- 24 | * Reflow using optimal instead of greedy algorithm (though in the IDE 25 | plugin you can turn on alternate formatting and invoking the action 26 | repeatedly alternates between the two modes.) 27 | * Command line script which can recursively format a whole source 28 | folder. 29 | * IDE plugin to format selected files or current comment. Preserves 30 | caret position in the current comment. Also hooks into the IDE 31 | formatting action. 32 | * Gradle plugin to format the source folders in the current project. 33 | * Block tags (like @param) are separated out from the main text, and 34 | subsequent lines are indented. Blank spaces between doc tags are 35 | removed. Preformatted text (indented 4 spaces or more) is left alone. 36 | * Can be run in a mode where it only reformats comments that were 37 | touched by the current git HEAD commit, or the currently staged files. 38 | Can also be passed specific line ranges to limit formatting to. 39 | * Multiline comments that would fit on a single line are converted to a 40 | single line comment (configurable via options) 41 | * Adds hanging indents for ordered and unordered indents. 42 | * Cleans up the double spaces left by the IntelliJ "Convert to Kotlin" 43 | action right before the closing comment token. 44 | * Removes trailing spaces. 45 | * Realigns table columns in Markdown tables and adds padding. 46 | * Reorders KDoc tags into a canonical order (for example placing 47 | * Can optionally convert various remaining HTML tags in the comments to 48 | the corresponding KDoc/markdown text. For example, \*\*bold** is 49 | converted into **bold**, \

is converted to a blank line, 50 | \

Heading\

is converted into # Heading, and so on. 51 | * Support for .editorconfig configuration files to automatically pick up 52 | line widths. It will normally use the line width configured for Kotlin 53 | files, but, if Markdown (.md) files are also configured, it will use 54 | that width as the maximum comment width. This allows you to have code 55 | line widths of for example 140 but limit comments to 70 characters 56 | (possibly indented). For code, avoiding line breaking is helpful, but 57 | for text, shorter lines are better for reading. 58 | 59 | Command Usage 60 | ------------- 61 | ``` 62 | $ kdoc-formatter 63 | Usage: kdoc-formatter [options] file(s) 64 | 65 | Options: 66 | --max-line-width= 67 | Sets the length of lines. Defaults to 72. 68 | --max-comment-width= 69 | Sets the maximum width of comments. This is helpful in a codebase 70 | with large line lengths, such as 140 in the IntelliJ codebase. 71 | Here, you don't want to limit the formatter maximum line width 72 | since indented code still needs to be properly formatted, but you 73 | also don't want comments to span 100+ characters, since that's less 74 | readable. Defaults to 72 (or max-line-width, if set lower than 72.) 75 | --hanging-indent= 76 | Sets the number of spaces to use for hanging indents, e.g. second 77 | and subsequent lines in a bulleted list or kdoc blog tag. 78 | --convert-markup 79 | Convert unnecessary HTML tags like < and > into < and >. 80 | --no-convert-markup 81 | Do not convert HTML markup into equivalent KDoc markup. 82 | --add-punctuation 83 | Add missing punctuation, such as a period at the end of a 84 | capitalized paragraph. 85 | --single-line-comments= 86 | With `collapse`, turns multi-line comments into a single line if it 87 | fits, and with `expand` it will always format commands with /** and 88 | */ on their own lines. The default is `collapse`. 89 | --align-table-columns 90 | Reformat tables such that the |column|separators| line up 91 | --no-align-table-columns 92 | Do not adjust formatting within table cells 93 | --order-doc-tags 94 | Move KDoc tags to the end of comments, and order them in a canonical 95 | order (@param before @return, and so on) 96 | --no-order-doc-tags 97 | Do not move or reorder KDoc tags 98 | --overlaps-git-changes= 99 | If git is on the path, and the command is invoked in a git 100 | repository, kdoc-formatter will invoke git to find the changes 101 | either in the HEAD commit or in the staged files, and will format 102 | only the KDoc comments that overlap these changes. 103 | --lines , --line 104 | Line range(s) to format, like 5:10 (1-based; default is all). Can be 105 | specified multiple times. 106 | --include-md-files 107 | Format markdown (*.md) files 108 | --greedy 109 | Instead of the optimal line breaking normally used by 110 | kdoc-formatter, do greedy line breaking instead 111 | --dry-run, -n 112 | Prints the paths of the files whose contents would change if the 113 | formatter were run normally. 114 | --quiet, -q 115 | Quiet mode 116 | --verbose, -v 117 | Verbose mode 118 | --help, -help, -h 119 | Print this usage statement. 120 | @ 121 | Read filenames from file. 122 | 123 | kdoc-formatter: Version 1.6.9 124 | https://github.com/tnorbye/kdoc-formatter 125 | ``` 126 | 127 | IntelliJ Plugin Usage 128 | --------------------- 129 | Install the IDE plugin. Open up the KDoc Formatting Options and inspect 130 | the settings to see if you want to change any of the options. By 131 | default, KDoc Formatter will use the line width configured for in the 132 | Kotlin editor code style for line breaking, but it will **also** look 133 | up the Markdown line width (typically 72) and use that as the maximum 134 | comment width. This means that by default, comments will be at most 72 135 | characters wide, even when using a code style which for example breaks 136 | at 140 characters. 137 | 138 | This is deliberate; comments are optimized for readability, and 139 | very long lines are not very readable -- which is why books are 140 | typically printed in portrait orientation, not landscape orientation. 141 | 142 | The code style line width is used to make sure that when the comment is 143 | in deeply nested code, it will still break comments into even shorter 144 | lines such that no line goes beyond the line limit. 145 | 146 | (Note that if your project uses .editorconfig files to specify 147 | indentation, those will be used instead of the Codestyle settings.) 148 | 149 | ![Screenshot](screenshot-settings.png) 150 | 151 | Other options here allow you to for example enable the option to 152 | add punctuation where it's missing, or to turn off features such as 153 | conversion of HTML tags into KDoc markup. 154 | 155 | The KDoc Formatter plugin integrates into the IDE's formatting actions, 156 | so if you reformat (for example via Code > Reformat Code), the comment 157 | will be reformatted along with the IDE's other code formatting. 158 | 159 | You can disable this in options, and instead explicitly invoke the 160 | formatter using Code > Reformat KDoc. You can configure a keyboard 161 | shortcut if you perform this action frequently (go to Preferences, 162 | search for Keymap, and then in the Keymap search field look for 163 | "KDoc", and then double click and choose Add Keyboard Shortcut. 164 | 165 | ![Screenshot](screenshot.png) 166 | 167 | The plugin is available from the JetBrains Marketplace at 168 | [https://plugins.jetbrains.com/plugin/15734-kotlin-kdoc-formatter](https://plugins.jetbrains.com/plugin/15734-kotlin-kdoc-formatter) 169 | 170 | Gradle Plugin Usage 171 | ------------------- 172 | The plugin is not yet distributed, so for now, download the zip file and 173 | install it somewhere, then add this to your build.gradle file: 174 | ```groovy 175 | buildscript { 176 | repositories { 177 | maven { url '/path/to/m2' } 178 | } 179 | dependencies { 180 | classpath "com.github.tnorbye.kdoc-formatter:kdocformatter:1.6.4" 181 | // (Sorry about the vanity URL -- 182 | // I tried to get kdoc-formatter:kdoc-formatter:1.6.4 but that 183 | // didn't meet the naming requirements for publishing: 184 | // https://issues.sonatype.org/browse/OSSRH-63191) 185 | } 186 | } 187 | plugins { 188 | id 'kdoc-formatter' 189 | } 190 | kdocformatter { 191 | options = "--single-line-comments=collapse --max-line-width=100" 192 | } 193 | ``` 194 | 195 | Or in build.gradle.kts: 196 | ```kts 197 | buildscript { 198 | repositories { 199 | maven { url = uri("/path/to/m2") } 200 | } 201 | dependencies { 202 | classpath("com.github.tnorbye.kdoc-formatter:kdocformatter:1.6.4") 203 | } 204 | } 205 | plugins { 206 | id("kdoc-formatter") 207 | } 208 | ``` 209 | 210 | Here, the [options] property lets you use any of the command line flags 211 | from the kdoc-formatter command. 212 | 213 | Building and testing 214 | -------------------- 215 | To create an installation of the command line tool, run 216 | 217 | ``` 218 | ./gradlew install 219 | ``` 220 | 221 | The installation will be located in cli/build/install/kdocformatter. 222 | 223 | To create a zip, run 224 | 225 | ``` 226 | ./gradlew zip 227 | ``` 228 | 229 | To build the plugin, run 230 | 231 | ``` 232 | ./gradlew :plugin:buildPlugin 233 | ``` 234 | 235 | The plugin will be located in plugin/build/distributions/. 236 | 237 | To run/test the plugin in the IDE, run 238 | 239 | ``` 240 | ./gradlew runIde 241 | ``` 242 | 243 | To reformat the source tree run 244 | 245 | ``` 246 | ./gradlew format 247 | ``` 248 | 249 | To build the Gradle plugin locally: 250 | ``` 251 | cd gradle-plugin 252 | ./gradlew publish 253 | ``` 254 | 255 | This will create a Maven local repository in m2/ which you can then 256 | point to from your consuming projects as shown in the Gradle Plugin 257 | Usage section above. 258 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/KDocFileFormatter.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import com.facebook.ktfmt.kdoc.CommentType 20 | import com.facebook.ktfmt.kdoc.FormattingTask 21 | import com.facebook.ktfmt.kdoc.KDocFormatter 22 | import com.facebook.ktfmt.kdoc.KDocFormattingOptions 23 | import com.facebook.ktfmt.kdoc.computeIndents 24 | import com.facebook.ktfmt.kdoc.getIndentSize 25 | import com.facebook.ktfmt.kdoc.isBlockComment 26 | import java.io.File 27 | import kotlin.math.min 28 | 29 | /** 30 | * This class attempts to iterate over an entire Kotlin source file and reformat all the KDocs it 31 | * finds within. This is based on some light-weight lexical analysis to identify comments. 32 | */ 33 | class KDocFileFormatter(private val options: KDocFileFormattingOptions) { 34 | init { 35 | EditorConfigs.root = options.formattingOptions 36 | } 37 | 38 | /** Formats the given file or directory recursively. */ 39 | fun formatFile(file: File): Int { 40 | if (file.isDirectory) { 41 | val name = file.name 42 | if (name.startsWith(".") && name != "." && name != "../") { 43 | // Skip .git and friends 44 | return 0 45 | } 46 | val files = file.listFiles() ?: return 0 47 | var count = 0 48 | for (f in files) { 49 | count += formatFile(f) 50 | } 51 | return count 52 | } 53 | 54 | val formatter = file.getFormatter() ?: return 0 55 | if (file.isFile && options.filter.includes(file)) { 56 | val original = file.readText() 57 | val reformatted = reformat(file, original, formatter) 58 | if (reformatted != original) { 59 | if (options.dryRun) { 60 | println(file.path) 61 | } else { 62 | if (options.verbose) { 63 | println(file.path) 64 | } 65 | file.writeText(reformatted) 66 | } 67 | return 1 68 | } 69 | } 70 | return 0 71 | } 72 | 73 | private fun File.getFormatter(): ((String, KDocFormattingOptions, File?) -> String)? { 74 | return if (path.endsWith(".kt")) { 75 | ::reformatKotlinFile 76 | } else if (options.includeMd && (path.endsWith(".md") || path.endsWith(".md.html"))) { 77 | ::reformatMarkdownFile 78 | } else { 79 | null 80 | } 81 | } 82 | 83 | fun reformatFile(file: File, source: String): String { 84 | return reformat(file, source, file.getFormatter()) 85 | } 86 | 87 | fun reformatSource(source: String, extension: String): String { 88 | val name = "path${if (extension.startsWith('.')) "" else "."}$extension" 89 | val file = File(name) 90 | return reformat(file, source, file.getFormatter()) 91 | } 92 | 93 | private fun reformat( 94 | file: File?, 95 | source: String, 96 | reformatter: ((String, KDocFormattingOptions, File?) -> String)? = file?.getFormatter() 97 | ): String { 98 | reformatter ?: return source 99 | val formattingOptions = 100 | (file?.let { EditorConfigs.getOptions(it) } ?: options.formattingOptions).copy() 101 | // Override editor config with any options explicitly passed on the command 102 | // line 103 | options.overrideOptions(formattingOptions) 104 | return reformatter(source, formattingOptions, file) 105 | } 106 | 107 | /** Reformats the given Markdown file. */ 108 | private fun reformatMarkdownFile( 109 | source: String, 110 | formattingOptions: KDocFormattingOptions, 111 | // Here such that this function has signature (String, KDocFormattingOptions, File?) 112 | @Suppress("UNUSED_PARAMETER") unused: File? = null 113 | ): String { 114 | // Just leverage the comment machinery here -- convert the markdown into 115 | // a kdoc comment, reformat that, and then uncomment it. Note that this 116 | // adds 3 characters to the line requirements (" * " on each line after the 117 | // opening "/**") so we duplicate the options and pre-add 3 to account for 118 | // this 119 | val formatter = KDocFormatter(formattingOptions.copy().apply { maxLineWidth += 3 }) 120 | val comment = "/**\n" + source.split("\n").joinToString(separator = "\n") { " * $it" } + "\n*/" 121 | val reformattedComment = 122 | " " + 123 | formatter 124 | .reformatComment(comment, "") 125 | .trim() 126 | .removePrefix("/**") 127 | .removeSuffix("*/") 128 | .trim() 129 | val reformatted = 130 | reformattedComment.split("\n").joinToString(separator = "\n") { 131 | if (it.startsWith(" * ")) { 132 | it.substring(3) 133 | } else if (it.startsWith(" *")) { 134 | "" 135 | } else if (it.startsWith("* ")) { 136 | it.substring(2) 137 | } else { 138 | it 139 | } 140 | } 141 | return reformatted 142 | } 143 | 144 | /** 145 | * Reformats the given Kotlin source file contents using the given [formattingOptions]. The 146 | * corresponding [file] can be consulted in case we want to limit filtering to particular lines 147 | * (for example modified git lines) 148 | */ 149 | private fun reformatKotlinFile( 150 | source: String, 151 | formattingOptions: KDocFormattingOptions, 152 | file: File? = null 153 | ): String { 154 | val sb = StringBuilder() 155 | val lexer = KotlinLexer(source) 156 | val tokens = 157 | lexer.findComments(options.kdocComments, options.blockComments, options.lineComments) 158 | val formatter = KDocFormatter(formattingOptions) 159 | val filter = options.filter 160 | var prev = 0 161 | for ((start, end) in tokens) { 162 | if (file == null || filter.overlaps(file, source, start, end)) { 163 | val comment = source.substring(start, end) 164 | if (skipComment(prev, comment)) { 165 | continue 166 | } 167 | 168 | // Include all the non-comments between previous comment end and here 169 | val segment = source.substring(prev, start) 170 | sb.append(segment) 171 | prev = end 172 | 173 | val formatted = 174 | format(sb, file, source, start, end, comment, lexer, formatter, formattingOptions) 175 | sb.append(formatted) 176 | } 177 | } 178 | sb.append(source.substring(prev, source.length)) 179 | 180 | return sb.toString() 181 | } 182 | 183 | private fun skipComment(prev: Int, comment: String): Boolean { 184 | // Let's leave license notices alone. 185 | if (prev == 0 && 186 | comment.isBlockComment() && 187 | (comment.contains("license", ignoreCase = true) || 188 | comment.contains("copyright", ignoreCase = true))) { 189 | return true 190 | } 191 | 192 | // Leave IntelliJ suppression comments alone 193 | if (comment.startsWith("//noinspection ")) { 194 | return true 195 | } 196 | 197 | return false 198 | } 199 | 200 | private fun format( 201 | sb: StringBuilder, 202 | file: File?, 203 | source: String, 204 | start: Int, 205 | end: Int, 206 | comment: String, 207 | lexer: KotlinLexer, 208 | formatter: KDocFormatter, 209 | formattingOptions: KDocFormattingOptions 210 | ): String { 211 | val originalIndent = getIndent(source, start) 212 | val suffix = !originalIndent.all { it.isWhitespace() } 213 | val (indent, secondaryIndent) = 214 | computeIndents(start, { offset -> source[offset] }, source.length) 215 | 216 | val formatted = 217 | try { 218 | val task = FormattingTask(formattingOptions, comment, indent, secondaryIndent) 219 | if (task.type == CommentType.KDOC) { 220 | task.orderedParameterNames = lexer.getParameterNames(end) ?: emptyList() 221 | } 222 | val formatted = formatter.reformatComment(task) 223 | 224 | // If it's the suffix of a line, see if it can fit there even when indented 225 | // with the previous code 226 | var addNewline = false 227 | val firstLineRemaining = 228 | min( 229 | formattingOptions.maxCommentWidth, 230 | formattingOptions.maxLineWidth - 231 | getIndentSize(task.initialIndent, formattingOptions)) 232 | val firstLine = 233 | formatted.substring( 234 | 0, formatted.indexOf('\n').let { if (it == -1) formatted.length else it }) 235 | val reformatted = 236 | if (suffix && task.type == CommentType.KDOC && formatted.contains("\n")) { 237 | addNewline = true 238 | formatted 239 | } else if (suffix && firstLineRemaining >= firstLine.length) { 240 | formatted 241 | } else if (suffix) { 242 | addNewline = true 243 | formatted 244 | } else { 245 | formatted 246 | } 247 | if (addNewline) { 248 | // Remove trailing whitespace on the line (e.g. separator between code and 249 | // /** */) 250 | while (sb.isNotEmpty() && sb[sb.length - 1].isWhitespace()) { 251 | sb.setLength(sb.length - 1) 252 | } 253 | sb.append('\n').append(secondaryIndent) 254 | } 255 | reformatted 256 | } catch (error: Throwable) { 257 | System.err.println("Failed formatting comment in $file:\n\"\"\"\n$comment\n\"\"\"") 258 | throw error 259 | } 260 | return formatted 261 | } 262 | 263 | private fun getIndent(source: String, start: Int): String { 264 | var i = start - 1 265 | while (i >= 0 && source[i] != '\n') { 266 | i-- 267 | } 268 | return source.substring(i + 1, start) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /library/src/main/kotlin/com/facebook/ktfmt/kdoc/Utilities.kt: -------------------------------------------------------------------------------- 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 | 22 | 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 | 30 | 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) */ 43 | 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 | 56 | fun String.isListItem(): Boolean { 57 | return startsWith("- ") || 58 | startsWith("* ") || 59 | startsWith("+ ") || 60 | firstOrNull()?.isDigit() == true && numberPattern.matcher(this).find() || 61 | startsWith("
  • ", ignoreCase = true) 62 | } 63 | 64 | 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 | 82 | fun String.isTodo(): Boolean { 83 | return startsWith("TODO:") || startsWith("TODO(") 84 | } 85 | 86 | fun String.isHeader(): Boolean { 87 | return startsWith("#") || startsWith(" ") 92 | } 93 | 94 | fun String.isDirectiveMarker(): Boolean { 95 | return startsWith("") 96 | } 97 | 98 | /** 99 | * Returns true if the string ends with a symbol that implies more text is coming, e.g. ":" or "," 100 | */ 101 | 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 | */ 110 | fun String.isLine(minCount: Int = 3): Boolean { 111 | return startsWith('-') && containsOnly('-', ' ') && count { it == '-' } >= minCount || 112 | startsWith('_') && containsOnly('_', ' ') && count { it == '_' } >= minCount 113 | } 114 | 115 | 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 | */ 145 | 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] == '[' || 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 | 190 | 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 | */ 211 | fun computeIndents(start: Int, lookup: (Int) -> Char, max: Int): Pair { 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 | */ 263 | 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 322 | fun > Iterable.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # KDoc Formatter Changelog 4 | 5 | ## [1.6.9] 6 | - IDE-only update: Marked compatible with IntelliJ IDEA 2025.3. 7 | 8 | ## [1.6.8] 9 | - IDE-only update: Marked compatible with IntelliJ IDEA 2025.2. 10 | 11 | ## [1.6.7] 12 | - Fix issue #104: Include type parameters in parameter list reordering 13 | - Fix issue #105: Make add punctuation apply to all paragraphs, not just last 14 | - IDE plugin fixes (replaced deprecated API calls) 15 | 16 | ## [1.6.6] 17 | - IDE-only update: Marked compatible with IntelliJ IDEA 2025.1. 18 | - Updated dependencies 19 | 20 | ## [1.6.5] 21 | - IDE-only update: Marked compatible with IntelliJ IDEA 2024.3. 22 | - Updated dependencies 23 | 24 | ## [1.6.4] 25 | - Switch continuation indent from 4 to 3. (IntelliJ's Dokka preview 26 | treats an indent of 4 or more as preformatted text even on a continued 27 | line; Dokka itself (and Markdown) does not. 28 | - Add ability to override the continuation indent in the IDE plugin 29 | settings. 30 | - Don't reorder `@sample` tags (backported 31 | https://github.com/facebook/ktfmt/issues/406) 32 | 33 | ## [1.6.3] 34 | - Mark plugin as compatible with K2 35 | 36 | ## [1.6.2] 37 | - IDE plugin update only: Compatibility with IntelliJ 2024.1 EAP. 38 | 39 | ## [1.6.1] 40 | - IDE plugin update only. 41 | 42 | ## [1.6.0] 43 | - Updated dependencies and fixed a few minor bugs, including issue 398 44 | from ktfmt. 45 | 46 | ## [1.5.9] 47 | - Compatibility with IntelliJ 2023.1 48 | 49 | ## [1.5.8] 50 | - Fixed a number of bugs: 51 | - #84: Line overrun when using closed-open interval notation 52 | - More gracefully handle unterminated [] references (for example when 53 | comment is using it in things like [closed, open) intervals) 54 | - Recognize and convert accidentally capitalized kdoc tags like @See 55 | - If you have a [ref] which spans a line such that the # ends up as a 56 | new line, don't treat this as a "# heading". 57 | - Make sure we don't line break at an expression starting with ">" 58 | since that would turn into a quoted line. 59 | - If you're using optimal line breaking and there's a really long, 60 | unbreakable word in the paragraph, switch that paragraph over to 61 | greedy line breaking (to make the paragraph better balanced since 62 | the really long word throws the algorithm off.) 63 | - Fix a few scenarios where markup conversion from

    and

    64 | wasn't converting everything. 65 | - Allow @property[name], not just @param[name] 66 | - The --hanging-indent flag now also sets the nested list indent 67 | (if >= 3) 68 | - Some minor code cleanup. 69 | 70 | ## [1.5.7] 71 | - Fixed the following bugs: 72 | - #76: Preserve newline style (CRLF on Windows) 73 | - #77: Preformatting error 74 | - #78: Preformatting stability 75 | - #79: Replace `{@param name}` with `[name]` 76 | 77 | ## [1.5.6] 78 | - Bugfix: the IDE plugin override line width setting was not working 79 | 80 | ## [1.5.5] 81 | - Fixed the following bugs: 82 | - #53: The collapse-single-line setting does not work in the IDE 83 | - #69: Paragraph + list (stability), variation 2 84 | - #70: Multi-line @link isn't converted if there's a # 85 | - #71: Make plugin dynamic 86 | - #72: @param with brackets is not supported 87 | - A bug where `

    ` paragraphs following a blank line would be 88 | deleted (without leaving a blank paragraph separator) 89 | - Changed heuristics around optimal or greedy line breaking in list 90 | items and for KDoc tag and TODO-item paragraph formatting. 91 | - The .editorconfig support is improved. It will now pick up the nearest 92 | applicable .editorconfig settings for a file, but any options 93 | explicitly set from the command line will override the .editor config. 94 | - Also, the "collapse documents that fit on a single line" option, 95 | instead of just defaulting to true, will now use the default 96 | specified for the equivalent setting for Java (if set), ensuring a 97 | more consistent codebase. (You can also set it for Kotlin using 98 | `ij_kotlin_doc_do_not_wrap_if_one_line`, though that option isn't 99 | supported in the IDE or by the Kotlin plugin currently.) 100 | - Preliminary support for formatting line comments and block comments 101 | (enabled via new flags, `--include-line-comments` and 102 | `--include-block-comments`.) 103 | - Misc IDE plugin improvements 104 | - `

    ` tags are converted into KDoc preformatted blocks
    105 | 
    106 | ## [1.5.4]
    107 | - Fix 9 bugs filed by the ktfmt project.
    108 | 
    109 | ## [1.5.3]
    110 | - Parameters are reordered to match the order in the corresponding
    111 |   method signature.
    112 | - In the IDE plugin, there are now options for explicitly specifying the
    113 |   line width and the comment width (and some other options cleanup).
    114 | - Some fixes to markdown wrapping
    115 | 
    116 | ## [1.5.2]
    117 | - Updates to the IDE plugin to allow configuring maxCommentWidth
    118 |   behavior. Fixes https://github.com/tnorbye/kdoc-formatter/issues/53
    119 | 
    120 | ## [1.5.1]
    121 | - Support for tables; by default it will realign columns and add edges,
    122 |   but this can be controlled via --align-table-columns and
    123 |   --no-align-table-columns. Horizontal padding is added inside the cells
    124 |   if there is space within the line.
    125 | - Move KDoc tags to the end of comments, and order them (e.g. @param
    126 |   before @return and so on). Can be enabled or disabled with
    127 |   --order-doc-tags and --no-order-doc-tags.
    128 | - Change default maxCommentWidth to 72 (was previously defaulting to the
    129 |   maxLineWidth.)
    130 | - Fix command line driver to properly handle nested string substitutions
    131 |   and to not get confused by single or double quotes backtick quoted
    132 |   function names.
    133 | - Fix a bug where formatting kdocs that started at the end of lines with
    134 |   code was not handled correctly
    135 | 
    136 | ## [1.5.0]
    137 | - A number of bug fixes across the formatter based on running the
    138 |   formatter on some larger code bases and inspecting the results, as
    139 |   well as diffing the HTML output rendered by Dokka.
    140 | - Improved handling for docs with slightly off indentation (e.g. an
    141 |   extra space here and there)
    142 | - Make sure we never break lines in the middle where the next word is
    143 |   ">" (which would be interpreted as a quoted string on the new line) or
    144 |   starts with "@" (which will be interpreted as a (possibly unknown)
    145 |   kdoc tag.)
    146 | - Fix interpretation of nested preformatted text (and revert
    147 |   optimization which skipped blank lines between these)
    148 | - Don't convert @linkplain tags to KDoc references, since Dokka will not
    149 |   render these as {@linkplain}.
    150 | - Handle TODO(string), and numbered lists separated with ) instead of .
    151 | - Revert the behavior from 1.4.4 which removed blank lines before
    152 |   preformatted text where the preformatted text was implicit via
    153 |   indentation.
    154 | 
    155 | ## [1.4.4]
    156 | - Fix bug in greedy line breaking which meant some lines were actually
    157 |   wider than allowed by the line limit
    158 | - Skip markup tag conversion for text inside `backticks` as was already
    159 |   done for preformatted text
    160 | - For lines that start with "

    " treat these as a paragraph start, as 161 | was already the case for lines containing only

    and drop these if 162 | markup conversion is enabled. 163 | - Don't add a blank line between text and preformatted text if the 164 | preceding text ends with a colon or a comma. 165 | 166 | ## [1.4.3] 167 | - Support for kotlinx-knit markers 168 | 169 | ## [1.4.2] 170 | - Fix two bugs: one related to nested lists, the other to embedded 171 | triple-backtick strings on lines 172 | 173 | ## [1.4.1] 174 | - Fix formatting nested bulleted lists 175 | (https://github.com/tnorbye/kdoc-formatter/issues/36) 176 | 177 | ## [1.4.0] 178 | - The IntelliJ plugin now applies KDoc formatting as part of regular 179 | formatting (Code > Format Code), not just via an explicit action. This 180 | is optional. 181 | - The markup conversion (if enabled) now converts [] and {@linkplain} 182 | tags to the KDoc equivalent. 183 | - Fix bug where preformatted text immediately following a TODO comment 184 | would be joined into the TODO. 185 | - Internally, updated from Java 8 and Kotlin 1.4 to Java 11 and Kotlin 186 | 1.6, various other dependencies, fixed some deprecations, and upgraded 187 | the plugin build and change log build scripts. 188 | 189 | ## [1.3.3] 190 | - Bug fix: Avoid line wrapping for text inside square brackets, 191 | contributed by Daniel Sturm 192 | 193 | ## [1.3.2] 194 | - A fix for kdoc-formatter issue #23, "Empty lines in preformatted 195 | paragraphs", contributed by Vittorio Massaro 196 | - A fix for a bug where in some corner cases a word could get dropped. 197 | 198 | ## [1.3.1] 199 | - Fixes a few bugs around markup conversion not producing the right 200 | number of blank lines for a

    , and adds a few more prefixes as 201 | non-breakable (e.g. if you have an em dash in your sentence -- like 202 | this -- we don't want the "--" to be placed at the beginning of a 203 | line.) 204 | - Adds an --add-punctuation command line flag and IDE setting to 205 | optionally add closing periods on capitalized paragraphs at the end of 206 | the comment. 207 | - Special cases TODO: comments (placing them in a block by themselves 208 | and using hanging indents). 209 | 210 | ## [1.3.0] 211 | - Many improves to the markdown handling, such as quoted blocks, 212 | headers, list continuations, etc. 213 | - Markup conversion, which until this point could convert inline tags 214 | such as **bold** into **bold**, etc, now handles many block level tags 215 | too, such as \

    , \

    , etc. 216 | - The IDE plugin can now also reformat line comments under the caret. 217 | (This is opt-in via options.) 218 | 219 | ## [1.2.0] 220 | - This version adds a settings panel to the IDE plugin where you can 221 | configure whether the plugin will alternate formatting modes on 222 | repeated invocation, as well as whether single line comments should be 223 | collapsed and whether to convert markup like bold to bold. (Note that 224 | line lengths will just use the IDE code style settings or 225 | .editorconfig files). 226 | - It also improves the code which preserves the caret across formatting 227 | actions to be a bit more accurate. 228 | 229 | ## [1.1.2] 230 | - This version adds basic support for .editorconfig files. It will use 231 | the configured line width for Kotlin source files, and it will also 232 | pick up the Markdown line length and set that as a maximum line width. 233 | 234 | ## [1.1.1] 235 | - Bug fix for combination of --overlaps-git-changes=staged and relative 236 | paths. 237 | - Also bails quickly if the given git commit contains no Kotlin source 238 | files. 239 | 240 | ## [1.1.0] 241 | - This version adds support for --max-comment-width, and for the Gradle 242 | plugin to be able to supply options (via kdocformatter.options = 243 | "--max-line-width=100 --max-comment-width=72" etc.) 244 | - It changes the Gradle plugin group id (since the previous one was 245 | rejected by Sonatype) and tweaks a few minor things. 246 | 247 | ## [1.0.0] 248 | - Command line script which can recursively format a whole source folder 249 | - IDE plugin to format selected files or current comment. Preserves 250 | caret position in the current comment. 251 | - Gradle plugin to format the source folders in the current project. 252 | - Block tags (like @param) are separated out from the main text, and 253 | subsequent lines are indented. Blank spaces between doc tags are 254 | removed. Preformatted text (indented 4 spaces or more) is left alone. 255 | - Can be run in a mode where it only reformats comments that were 256 | touched by the current git HEAD commit, or the currently staged files. 257 | Can also be passed specific line ranges to limit formatting to. 258 | - Multiline comments that would fit on a single line are converted to a 259 | single line comment (configurable via options) 260 | - Adds hanging indents for ordered and unordered indents. 261 | - Cleans up the double spaces left by the IntelliJ "Convert to Kotlin" 262 | action right before the closing comment token. 263 | - Removes trailing spaces. 264 | -------------------------------------------------------------------------------- /cli/src/main/kotlin/kdocformatter/cli/KDocFileFormattingOptions.kt: -------------------------------------------------------------------------------- 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 kdocformatter.cli 18 | 19 | import com.facebook.ktfmt.kdoc.KDocFormattingOptions 20 | import com.facebook.ktfmt.kdoc.Version 21 | import java.io.File 22 | import kotlin.system.exitProcess 23 | 24 | /** 25 | * Options for configuring whole files or directories. The [formattingOptions] property specifies 26 | * the KDoc specific formatting options; the rest of the options are related to how to (and whether 27 | * to) process files. 28 | */ 29 | class KDocFileFormattingOptions { 30 | var dryRun: Boolean = false 31 | var verbose: Boolean = false 32 | var quiet: Boolean = false 33 | var gitPath: String = "" 34 | var filter = RangeFilter() // default accepts all 35 | var files = listOf() 36 | var formattingOptions: KDocFormattingOptions = KDocFormattingOptions() 37 | var kdocComments = true 38 | var blockComments = false 39 | var lineComments = false 40 | var gitStaged = false 41 | var gitHead = false 42 | var includeMd: Boolean = false 43 | 44 | /** 45 | * Applies any options explicitly specified via command line options to the given formatting 46 | * options. 47 | */ 48 | var overrideOptions: (KDocFormattingOptions) -> Unit = {} 49 | 50 | companion object { 51 | fun parse(args: Array): KDocFileFormattingOptions { 52 | val options = KDocFileFormattingOptions() 53 | var i = 0 54 | val files = mutableListOf() 55 | options.files = files 56 | val rangeLines = mutableListOf() 57 | 58 | fun parseInt(s: String): Int = s.toIntOrNull() ?: error("$s is not a number") 59 | 60 | var lineWidth: Int? = null 61 | var commentWidth: Int? = null 62 | var hangingIndent: Int? = null 63 | var collapseSingleLine: Boolean? = null 64 | var alignTableColumns: Boolean? = null 65 | var convertMarkup: Boolean? = null 66 | var addPunctuation: Boolean? = null 67 | var optimal: Boolean? = null 68 | var orderDocTags: Boolean? = null 69 | 70 | while (i < args.size) { 71 | val arg = args[i] 72 | i++ 73 | when { 74 | arg == "--help" || arg == "-help" || arg == "-h" -> println(usage()) 75 | arg == "--max-line-width" || arg == "--line-width" || arg == "--right-margin" -> 76 | lineWidth = parseInt(args[i++]) 77 | arg.startsWith("--max-line-width=") -> 78 | lineWidth = parseInt(arg.substring("--max-line-width=".length)) 79 | arg == "--max-comment-width" -> commentWidth = parseInt(args[i++]) 80 | arg.startsWith("--max-comment-width=") -> 81 | commentWidth = parseInt(arg.substring("--max-comment-width=".length)) 82 | arg == "--hanging-indent" -> hangingIndent = parseInt(args[i++]) 83 | arg.startsWith("--hanging-indent=") -> 84 | hangingIndent = parseInt(arg.substring("--hanging-indent=".length)) 85 | arg == "--convert-markup" -> convertMarkup = true 86 | arg == "--no-convert-markup" || 87 | arg == "--convert-markup=false" || 88 | arg == "--convert-markup=off" -> convertMarkup = false 89 | arg == "--align-table-columns" -> alignTableColumns = true 90 | arg == "--no-align-table-columns" || 91 | arg == "--align-table-columns=false" || 92 | arg == "--align-table-columns=off" -> alignTableColumns = false 93 | arg == "--order-doc-tags" -> orderDocTags = true 94 | arg == "--no-order-doc-tags" || 95 | arg == "--order-doc-tags=false" || 96 | arg == "--order-doc-tags=off" -> orderDocTags = false 97 | arg == "--add-punctuation" -> addPunctuation = true 98 | arg.startsWith("--single-line-comments=collapse") || arg == "--single-line-comments" -> 99 | collapseSingleLine = true 100 | arg.startsWith("--single-line-comments=expand") -> collapseSingleLine = false 101 | arg.startsWith("--single-line-comments=") -> 102 | error("Only `collapse` and `expand` are supported for --single-line-comments") 103 | arg == "--overlaps-git-changes=HEAD" -> options.gitHead = true 104 | arg == "--overlaps-git-changes=staged" -> options.gitStaged = true 105 | arg == "--include-block-comments" -> options.blockComments = true 106 | arg == "--include-line-comments" -> options.lineComments = true 107 | arg == "--include-kdoc-comments" -> options.kdocComments = true 108 | arg == "--exclude-block-comments" -> options.blockComments = false 109 | arg == "--exclude-line-comments" -> options.lineComments = false 110 | arg == "--exclude-kdoc-comments" -> options.kdocComments = false 111 | arg.startsWith("--overlaps-git-changes=") -> 112 | error("Only `HEAD` and `staged` are supported for --overlaps-git-changes") 113 | arg == "--lines" || arg == "--line" -> rangeLines.add(args[i++]) 114 | arg.startsWith("--lines=") -> rangeLines.add(arg.substring("--lines=".length)) 115 | arg == "--dry-run" || arg == "-n" -> options.dryRun = true 116 | arg == "--quiet" || arg == "-q" -> options.quiet = true 117 | arg == "--verbose" || arg == "-v" -> options.verbose = true 118 | arg == "--git-path" -> options.gitPath = args[i++] 119 | arg.startsWith("--git-path=") -> options.gitPath = arg.substring("--git-path=".length) 120 | arg == "--include-md-files" -> options.includeMd = true 121 | arg == "--greedy" -> optimal = false 122 | else -> { 123 | val paths = 124 | if (arg.startsWith("@")) { 125 | val f = File(arg.substring(1)) 126 | if (!f.exists()) { 127 | System.err.println("$f does not exist") 128 | exitProcess(-1) 129 | } 130 | f.readText().split('\n').toList() 131 | } else if (arg.startsWith("-")) { 132 | System.err.println("Unrecognized flag `$arg`") 133 | exitProcess(-1) 134 | } else { 135 | listOf(arg) 136 | } 137 | for (path in paths) { 138 | if (path.isBlank()) { 139 | continue 140 | } 141 | val file = File(arg.trim()).canonicalFile 142 | if (!file.exists()) { 143 | error("$file does not exist") 144 | } 145 | files.add(file) 146 | } 147 | } 148 | } 149 | } 150 | 151 | if (files.size == 1 && files[0].isFile) { 152 | // If you directly try to format a Markdown file, don't require specifying 153 | // the flag for that. 154 | val path = files[0].path 155 | if (path.endsWith(".md") || path.endsWith(".md.html")) { 156 | options.includeMd = true 157 | } 158 | } 159 | 160 | if ((options.gitHead || options.gitStaged) && files.isNotEmpty()) { 161 | // Delayed initialization because the git path and the paths to the 162 | // repository is typically specified after this flag 163 | val filters = mutableListOf() 164 | if (options.gitHead) { 165 | GitRangeFilter.create(options.gitPath, files.first(), false)?.let { filters.add(it) } 166 | ?: error("Could not create git range filter for the staged files") 167 | } 168 | if (options.gitStaged) { 169 | GitRangeFilter.create(options.gitPath, files.first(), true)?.let { filters.add(it) } 170 | ?: error("Could not create git range filter for the files in HEAD") 171 | } 172 | options.filter = 173 | if (filters.size == 2) { 174 | UnionFilter(filters) 175 | } else { 176 | filters.first() 177 | } 178 | } else if (rangeLines.isNotEmpty()) { 179 | if (files.size != 1 || !files[0].isFile) { 180 | error("The --lines option can only be used with a single file") 181 | } else { 182 | options.filter = LineRangeFilter.fromRangeStrings(files[0], rangeLines) 183 | } 184 | } 185 | 186 | options.overrideOptions = { o -> 187 | lineWidth?.let { o.maxLineWidth = it } 188 | commentWidth?.let { o.maxCommentWidth = it } 189 | collapseSingleLine?.let { o.collapseSingleLine = it } 190 | hangingIndent?.let { 191 | o.hangingIndent = it 192 | if (it >= 3) o.nestedListIndent = it 193 | } 194 | alignTableColumns?.let { o.alignTableColumns = it } 195 | convertMarkup?.let { o.convertMarkup = it } 196 | addPunctuation?.let { o.addPunctuation = it } 197 | optimal?.let { o.optimal = it } 198 | orderDocTags?.let { o.orderDocTags = it } 199 | } 200 | options.overrideOptions(options.formattingOptions) 201 | 202 | return options 203 | } 204 | 205 | fun usage(): String { 206 | val options = 207 | listOf( 208 | "--max-line-width=" to 209 | """ 210 | Sets the length of lines. Defaults to 72.""", 211 | "--max-comment-width=" to 212 | """ 213 | Sets the maximum width of comments. This is helpful in a codebase 214 | with large line lengths, such as 140 in the IntelliJ codebase. Here, 215 | you don't want to limit the formatter maximum line width since 216 | indented code still needs to be properly formatted, but you also 217 | don't want comments to span 100+ characters, since that's less 218 | readable. Defaults to 72 (or max-line-width, if set lower than 72.) 219 | """, 220 | "--hanging-indent=" to 221 | """ 222 | Sets the number of spaces to use for hanging indents, e.g. second 223 | and subsequent lines in a bulleted list or kdoc blog tag.""", 224 | "--convert-markup" to 225 | """ 226 | Convert unnecessary HTML tags like < and > into < and >.""", 227 | "--no-convert-markup" to 228 | """ 229 | Do not convert HTML markup into equivalent KDoc markup.""", 230 | "--add-punctuation" to 231 | """ 232 | Add missing punctuation, such as a period at the end of a capitalized 233 | paragraph.""", 234 | "--single-line-comments=" to 235 | """ 236 | With `collapse`, turns multi-line comments into a single line if it 237 | fits, and with `expand` it will always format commands with /** and 238 | */ on their own lines. The default is `collapse`.""", 239 | "--align-table-columns" to 240 | """ 241 | Reformat tables such that the |column|separators| line up""", 242 | "--no-align-table-columns" to 243 | """ 244 | Do not adjust formatting within table cells""", 245 | "--order-doc-tags" to 246 | """ 247 | Move KDoc tags to the end of comments, and order them in a canonical 248 | order (@param before @return, and so on)""", 249 | "--no-order-doc-tags" to 250 | """ 251 | Do not move or reorder KDoc tags""", 252 | "--include-block-comments" to 253 | """ 254 | Format /* block comments */ as well 255 | """, 256 | "--include-line-comments" to 257 | """ 258 | Format // line comments as well 259 | """, 260 | "--overlaps-git-changes=" to 261 | """ 262 | If git is on the path, and the command is invoked in a git 263 | repository, kdoc-formatter will invoke git to find the changes either 264 | in the HEAD commit or in the staged files, and will format only the 265 | KDoc comments that overlap these changes.""", 266 | "--lines , --line " to 267 | """ 268 | Line range(s) to format, like 5:10 (1-based; default is all). Can be 269 | specified multiple times.""", 270 | "--include-md-files" to 271 | """ 272 | Format markdown (*.md) files""", 273 | "--greedy" to 274 | """ 275 | Instead of the optimal line breaking normally used by kdoc-formatter, 276 | do greedy line breaking instead""", 277 | "--dry-run, -n" to 278 | """ 279 | Prints the paths of the files whose contents would change if the 280 | formatter were run normally.""", 281 | "--quiet, -q" to 282 | """ 283 | Quiet mode""", 284 | "--verbose, -v" to 285 | """ 286 | Verbose mode""", 287 | "--help, -help, -h" to 288 | """ 289 | Print this usage statement.""", 290 | "@" to 291 | """ 292 | Read filenames from file.""") 293 | 294 | val fileOptions = KDocFileFormattingOptions() 295 | fileOptions.includeMd = true 296 | fileOptions.formattingOptions = KDocFormattingOptions(68) // 72 minus 4 for indentation 297 | val formatter = KDocFileFormatter(fileOptions) 298 | 299 | val formattedOptions = 300 | options.joinToString("\n") { 301 | val (arg, desc) = it 302 | val trimmed = desc.trimIndent().trim() 303 | val source = 304 | formatter.reformatSource(trimmed, ".md").split("\n").joinToString("\n") { line -> 305 | " $line" 306 | } 307 | arg + "\n" + source 308 | } 309 | 310 | return """ 311 | Usage: kdoc-formatter [options] file(s) 312 | 313 | Options: 314 | """ 315 | .trimIndent() + 316 | "\n" + 317 | formattedOptions + 318 | "\n\n" + 319 | """ 320 | kdoc-formatter: Version ${Version.versionString} 321 | https://github.com/tnorbye/kdoc-formatter 322 | """ 323 | .trimIndent() 324 | } 325 | } 326 | } 327 | --------------------------------------------------------------------------------