|  | 
|  | 1 | +package dotty.tools.languageserver | 
|  | 2 | + | 
|  | 3 | +import dotty.tools.dotc.core.Comments.{Comment, CommentsContext} | 
|  | 4 | +import dotty.tools.dotc.core.Contexts.Context | 
|  | 5 | +import dotty.tools.dotc.core.Names.TermName | 
|  | 6 | +import dotty.tools.dotc.core.Symbols._ | 
|  | 7 | +import dotty.tools.dotc.util.CommentParsing | 
|  | 8 | + | 
|  | 9 | +import scala.collection.immutable.ListMap | 
|  | 10 | +import scala.util.matching.Regex | 
|  | 11 | + | 
|  | 12 | +/** | 
|  | 13 | + * A parsed doc comment. | 
|  | 14 | + * | 
|  | 15 | + * @param comment The doc comment to parse | 
|  | 16 | + */ | 
|  | 17 | +class ParsedComment(val comment: Comment) { | 
|  | 18 | + | 
|  | 19 | +  /** | 
|  | 20 | +   * The bounds of a section that represents the [start; end[ char offset | 
|  | 21 | +   * of the section within this comment's `content`. | 
|  | 22 | +   */ | 
|  | 23 | +  private type Bounds = (Int, Int) | 
|  | 24 | + | 
|  | 25 | +  /** The content of this comment, after expansion if possible. */ | 
|  | 26 | +  val content: String = comment.expandedBody.getOrElse(comment.raw) | 
|  | 27 | + | 
|  | 28 | +  /** An index that marks all sections boundaries */ | 
|  | 29 | +  private lazy val tagIndex: List[Bounds] = CommentParsing.tagIndex(content) | 
|  | 30 | + | 
|  | 31 | +  /** | 
|  | 32 | +   * Maps a parameter name to the bounds of its doc | 
|  | 33 | +   * | 
|  | 34 | +   * @see paramDoc | 
|  | 35 | +   */ | 
|  | 36 | +  private lazy val paramDocs: Map[String, Bounds] = CommentParsing.paramDocs(content, "@param", tagIndex) | 
|  | 37 | + | 
|  | 38 | +  /** | 
|  | 39 | +   * The "main" documentation for this comment. That is, the comment before any section starts. | 
|  | 40 | +   */ | 
|  | 41 | +  lazy val mainDoc: String = { | 
|  | 42 | +    val doc = tagIndex match { | 
|  | 43 | +      case Nil => content.stripSuffix("*/") | 
|  | 44 | +      case (start, _) :: _ => content.slice(0, start) | 
|  | 45 | +    } | 
|  | 46 | +    clean(doc.stripPrefix("/**")) | 
|  | 47 | +  } | 
|  | 48 | + | 
|  | 49 | +  /** | 
|  | 50 | +   * Renders this comment as markdown. | 
|  | 51 | +   * | 
|  | 52 | +   * The different sections are formatted according to the maping in `knownTags`. | 
|  | 53 | +   */ | 
|  | 54 | +  def renderAsMarkdown: String = { | 
|  | 55 | +    val buf = new StringBuilder | 
|  | 56 | +    buf.append(mainDoc + System.lineSeparator + System.lineSeparator) | 
|  | 57 | +    val groupedSections = CommentParsing.groupedSections(content, tagIndex) | 
|  | 58 | + | 
|  | 59 | +    for { | 
|  | 60 | +      (tag, formatter) <- ParsedComment.knownTags | 
|  | 61 | +      boundss <- groupedSections.get(tag) | 
|  | 62 | +      texts = boundss.map { (start, end) => clean(content.slice(start, end)) } | 
|  | 63 | +      formatted <- formatter(texts) | 
|  | 64 | +    } { | 
|  | 65 | +      buf.append(formatted) | 
|  | 66 | +      buf.append(System.lineSeparator) | 
|  | 67 | +    } | 
|  | 68 | + | 
|  | 69 | +    buf.toString | 
|  | 70 | +  } | 
|  | 71 | + | 
|  | 72 | +  /** | 
|  | 73 | +   * The `@param` section corresponding to `name`. | 
|  | 74 | +   * | 
|  | 75 | +   * @param name The parameter name whose documentation to extract. | 
|  | 76 | +   * @return The formatted documentation corresponding to `name`. | 
|  | 77 | +   */ | 
|  | 78 | +  def paramDoc(name: TermName): Option[String] = paramDocs.get(name.toString).map { (start, end) => | 
|  | 79 | +    val rawContent = content.slice(start, end) | 
|  | 80 | +    val docContent = ParsedComment.prefixRegex.replaceFirstIn(rawContent, "") | 
|  | 81 | +    clean(docContent) | 
|  | 82 | +  } | 
|  | 83 | + | 
|  | 84 | +  /** | 
|  | 85 | +   * Cleans `str`: remove prefixing `*` and trim the string. | 
|  | 86 | +   * | 
|  | 87 | +   * @param str The string to clean | 
|  | 88 | +   * @return The cleaned string. | 
|  | 89 | +   */ | 
|  | 90 | +  private def clean(str: String): String = str.stripMargin('*').trim | 
|  | 91 | +} | 
|  | 92 | + | 
|  | 93 | +object ParsedComment { | 
|  | 94 | + | 
|  | 95 | +  /** | 
|  | 96 | +   * Return the `ParsedComment` associated with `symbol`, if it exists. | 
|  | 97 | +   * | 
|  | 98 | +   * @param symbol The symbol for which to retrieve the documentation | 
|  | 99 | +   * @return If it exists, the `ParsedComment` for `symbol`. | 
|  | 100 | +   */ | 
|  | 101 | +  def docOf(symbol: Symbol)(implicit ctx: Context): Option[ParsedComment] = { | 
|  | 102 | +    val documentedSymbol = if (symbol.isPrimaryConstructor) symbol.owner else symbol | 
|  | 103 | +    for { docCtx <- ctx.docCtx | 
|  | 104 | +          comment <- docCtx.docstring(documentedSymbol) | 
|  | 105 | +    } yield new ParsedComment(comment) | 
|  | 106 | +  } | 
|  | 107 | + | 
|  | 108 | +  private val prefixRegex = """@param\s+\w+\s+""".r | 
|  | 109 | + | 
|  | 110 | +  /** A mapping from tag name to `TagFormatter` */ | 
|  | 111 | +  private val knownTags: ListMap[String, TagFormatter] = ListMap( | 
|  | 112 | +    "@tparam"  -> TagFormatter("Type Parameters", toDescriptionList), | 
|  | 113 | +    "@param"   -> TagFormatter("Parameters", toDescriptionList), | 
|  | 114 | +    "@return"  -> TagFormatter("Returns", toMarkdownList), | 
|  | 115 | +    "@throws"  -> TagFormatter("Throws", toDescriptionList), | 
|  | 116 | +    "@see"     -> TagFormatter("See Also", toMarkdownList), | 
|  | 117 | +    "@example" -> TagFormatter("Examples", toCodeFences("scala")), | 
|  | 118 | +    "@usecase" -> TagFormatter("Usecases", toCodeFences("scala")), | 
|  | 119 | +    "@note"    -> TagFormatter("Note", toMarkdownList), | 
|  | 120 | +    "@author"  -> TagFormatter("Authors", toMarkdownList), | 
|  | 121 | +    "@since"   -> TagFormatter("Since", toMarkdownList), | 
|  | 122 | +    "@version" -> TagFormatter("Version", toMarkdownList) | 
|  | 123 | +  ) | 
|  | 124 | + | 
|  | 125 | +  /** | 
|  | 126 | +   * Formats a list of items into a list describing them. | 
|  | 127 | +   * | 
|  | 128 | +   * Each element is assumed to consist of a first word, which is the item being described. The rest | 
|  | 129 | +   * is the description of the item. | 
|  | 130 | +   * | 
|  | 131 | +   * @param items The items to format into a list. | 
|  | 132 | +   * @return A markdown list of descriptions. | 
|  | 133 | +   */ | 
|  | 134 | +  private def toDescriptionList(items: List[String]): String = { | 
|  | 135 | +    val formattedItems = items.map { p => | 
|  | 136 | +      val name :: rest = p.split(" ", 2).toList | 
|  | 137 | +      s"**$name** ${rest.mkString("").trim}" | 
|  | 138 | +    } | 
|  | 139 | +    toMarkdownList(formattedItems) | 
|  | 140 | +  } | 
|  | 141 | + | 
|  | 142 | +  /** | 
|  | 143 | +   * Formats a list of items into a markdown list. | 
|  | 144 | +   * | 
|  | 145 | +   * @param items The items to put in a list. | 
|  | 146 | +   * @return The list of items, in markdown. | 
|  | 147 | +   */ | 
|  | 148 | +  private def toMarkdownList(items: List[String]): String = { | 
|  | 149 | +    val formattedItems = items.map(_.lines.mkString(System.lineSeparator + "   ")) | 
|  | 150 | +    formattedItems.mkString(" - ", System.lineSeparator + " - ", "") | 
|  | 151 | +  } | 
|  | 152 | + | 
|  | 153 | +  /** | 
|  | 154 | +   * Wrap each of `snippets` into a markdown code fence, using `language`. All the code fences are | 
|  | 155 | +   * put into a markdown list. | 
|  | 156 | +   * | 
|  | 157 | +   * @param language The language to use for the code fences | 
|  | 158 | +   * @param snippets The list of snippets to format. | 
|  | 159 | +   * @return A markdown list of code fences. | 
|  | 160 | +   * @see toCodeFence | 
|  | 161 | +   */ | 
|  | 162 | +  private def toCodeFences(language: String)(snippets: List[String]): String = | 
|  | 163 | +    toMarkdownList(snippets.map(toCodeFence(language))) | 
|  | 164 | + | 
|  | 165 | +  /** | 
|  | 166 | +   * Wraps `snippet` in a markdown code fence, using `language`. | 
|  | 167 | +   * | 
|  | 168 | +   * @param language The language to use. | 
|  | 169 | +   * @param snippet  The code snippet | 
|  | 170 | +   * @return `snippet`, wrapped in a code fence. | 
|  | 171 | +   */ | 
|  | 172 | +  private def toCodeFence(language: String)(snippet: String): String = | 
|  | 173 | +    s"""```$language | 
|  | 174 | +       |$snippet | 
|  | 175 | +       |```""".stripMargin | 
|  | 176 | + | 
|  | 177 | +  /** | 
|  | 178 | +   * Format the elements of documentation associated with a given tag using `fn`, and starts the | 
|  | 179 | +   * section with `title`. | 
|  | 180 | +   * | 
|  | 181 | +   * @param title The title to give to the formatted items. | 
|  | 182 | +   * @param fn    The formatting function to use. | 
|  | 183 | +   */ | 
|  | 184 | +  private case class TagFormatter(title: String, fn: List[String] => String) { | 
|  | 185 | + | 
|  | 186 | +    /** | 
|  | 187 | +     * Format `item` using `fn` if `items` is not empty. | 
|  | 188 | +     * | 
|  | 189 | +     * @param items The items to format | 
|  | 190 | +     * @return If items is not empty, the items formatted using `fn`. | 
|  | 191 | +     */ | 
|  | 192 | +    def apply(items: List[String]): Option[String] = items match { | 
|  | 193 | +      case Nil => | 
|  | 194 | +        None | 
|  | 195 | +      case items => | 
|  | 196 | +        Some(s"""$title: | 
|  | 197 | +                |${fn(items)} | 
|  | 198 | +                |""".stripMargin) | 
|  | 199 | +    } | 
|  | 200 | +  } | 
|  | 201 | + | 
|  | 202 | +} | 
0 commit comments