layouter.nim 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #
  2. #
  3. # The Nim Compiler
  4. # (c) Copyright 2018 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## Layouter for nimpretty.
  10. import idents, lexer, lineinfos, llstream, options, msgs, strutils,
  11. pathutils
  12. from os import changeFileExt
  13. const
  14. MaxLineLen = 80
  15. LineCommentColumn = 30
  16. type
  17. SplitKind = enum
  18. splitComma, splitParLe, splitAnd, splitOr, splitIn, splitBinary
  19. SemicolonKind = enum
  20. detectSemicolonKind, useSemicolon, dontTouch
  21. Emitter* = object
  22. config: ConfigRef
  23. fid: FileIndex
  24. lastTok: TTokType
  25. inquote, lastTokWasTerse: bool
  26. semicolons: SemicolonKind
  27. col, lastLineNumber, lineSpan, indentLevel, indWidth: int
  28. keepIndents*: int
  29. doIndentMore*: int
  30. content: string
  31. indentStack: seq[int]
  32. fixedUntil: int # marks where we must not go in the content
  33. altSplitPos: array[SplitKind, int] # alternative split positions
  34. proc openEmitter*(em: var Emitter, cache: IdentCache;
  35. config: ConfigRef, fileIdx: FileIndex) =
  36. let fullPath = Absolutefile config.toFullPath(fileIdx)
  37. em.indWidth = getIndentWidth(fileIdx, llStreamOpen(fullPath, fmRead),
  38. cache, config)
  39. if em.indWidth == 0: em.indWidth = 2
  40. em.config = config
  41. em.fid = fileIdx
  42. em.lastTok = tkInvalid
  43. em.inquote = false
  44. em.col = 0
  45. em.content = newStringOfCap(16_000)
  46. em.indentStack = newSeqOfCap[int](30)
  47. em.indentStack.add 0
  48. em.lastLineNumber = 1
  49. proc closeEmitter*(em: var Emitter) =
  50. if fileExists(em.config.outFile) and readFile(em.config.outFile.string) == em.content:
  51. discard "do nothing, see #9499"
  52. return
  53. var f = llStreamOpen(em.config.outFile, fmWrite)
  54. if f == nil:
  55. rawMessage(em.config, errGenerated, "cannot open file: " & em.config.outFile.string)
  56. return
  57. f.llStreamWrite em.content
  58. llStreamClose(f)
  59. proc countNewlines(s: string): int =
  60. result = 0
  61. for i in 0..<s.len:
  62. if s[i] == '\L': inc result
  63. proc calcCol(em: var Emitter; s: string) =
  64. var i = s.len-1
  65. em.col = 0
  66. while i >= 0 and s[i] != '\L':
  67. dec i
  68. inc em.col
  69. template wr(x) =
  70. em.content.add x
  71. inc em.col, x.len
  72. template goodCol(col): bool = col in 40..MaxLineLen
  73. const
  74. openPars = {tkParLe, tkParDotLe,
  75. tkBracketLe, tkBracketLeColon, tkCurlyDotLe,
  76. tkCurlyLe}
  77. splitters = openPars + {tkComma, tkSemicolon}
  78. oprSet = {tkOpr, tkDiv, tkMod, tkShl, tkShr, tkIn, tkNotin, tkIs,
  79. tkIsnot, tkNot, tkOf, tkAs, tkDotDot, tkAnd, tkOr, tkXor}
  80. template rememberSplit(kind) =
  81. if goodCol(em.col):
  82. em.altSplitPos[kind] = em.content.len
  83. template moreIndent(em): int =
  84. (if em.doIndentMore > 0: em.indWidth*2 else: em.indWidth)
  85. proc softLinebreak(em: var Emitter, lit: string) =
  86. # XXX Use an algorithm that is outlined here:
  87. # https://llvm.org/devmtg/2013-04/jasper-slides.pdf
  88. # +2 because we blindly assume a comma or ' &' might follow
  89. if not em.inquote and em.col+lit.len+2 >= MaxLineLen:
  90. if em.lastTok in splitters:
  91. while em.content.len > 0 and em.content[em.content.high] == ' ':
  92. setLen(em.content, em.content.len-1)
  93. wr("\L")
  94. em.col = 0
  95. for i in 1..em.indentLevel+moreIndent(em): wr(" ")
  96. else:
  97. # search backwards for a good split position:
  98. for a in mitems(em.altSplitPos):
  99. if a > em.fixedUntil:
  100. var spaces = 0
  101. while a+spaces < em.content.len and em.content[a+spaces] == ' ':
  102. inc spaces
  103. if spaces > 0: delete(em.content, a, a+spaces-1)
  104. em.col = em.content.len - a
  105. let ws = "\L" & repeat(' ', em.indentLevel+moreIndent(em))
  106. em.content.insert(ws, a)
  107. a = -1
  108. break
  109. proc emitTok*(em: var Emitter; L: TLexer; tok: TToken) =
  110. template endsInWhite(em): bool =
  111. em.content.len == 0 or em.content[em.content.high] in {' ', '\L'}
  112. template endsInAlpha(em): bool =
  113. em.content.len > 0 and em.content[em.content.high] in SymChars+{'_'}
  114. proc emitComment(em: var Emitter; tok: TToken) =
  115. let lit = strip fileSection(em.config, em.fid, tok.commentOffsetA, tok.commentOffsetB)
  116. em.lineSpan = countNewlines(lit)
  117. if em.lineSpan > 0: calcCol(em, lit)
  118. if not endsInWhite(em):
  119. wr(" ")
  120. if em.lineSpan == 0 and max(em.col, LineCommentColumn) + lit.len <= MaxLineLen:
  121. for i in 1 .. LineCommentColumn - em.col: wr(" ")
  122. wr lit
  123. if tok.tokType == tkComment and tok.literal.startsWith("#!nimpretty"):
  124. case tok.literal
  125. of "#!nimpretty off":
  126. inc em.keepIndents
  127. wr("\L")
  128. em.lastLineNumber = tok.line + 1
  129. of "#!nimpretty on":
  130. dec em.keepIndents
  131. em.lastLineNumber = tok.line
  132. wr("\L")
  133. #for i in 1 .. tok.indent: wr " "
  134. wr tok.literal
  135. em.col = 0
  136. em.lineSpan = 0
  137. return
  138. var preventComment = false
  139. if tok.tokType == tkComment and tok.line == em.lastLineNumber and tok.indent >= 0:
  140. # we have an inline comment so handle it before the indentation token:
  141. emitComment(em, tok)
  142. preventComment = true
  143. em.fixedUntil = em.content.high
  144. elif tok.indent >= 0:
  145. if em.lastTok in (splitters + oprSet) or em.keepIndents > 0:
  146. em.indentLevel = tok.indent
  147. else:
  148. if tok.indent > em.indentStack[^1]:
  149. em.indentStack.add tok.indent
  150. else:
  151. # dedent?
  152. while em.indentStack.len > 1 and em.indentStack[^1] > tok.indent:
  153. discard em.indentStack.pop()
  154. em.indentLevel = em.indentStack.high * em.indWidth
  155. #[ we only correct the indentation if it is not in an expression context,
  156. so that code like
  157. const splitters = {tkComma, tkSemicolon, tkParLe, tkParDotLe,
  158. tkBracketLe, tkBracketLeColon, tkCurlyDotLe,
  159. tkCurlyLe}
  160. is not touched.
  161. ]#
  162. # remove trailing whitespace:
  163. while em.content.len > 0 and em.content[em.content.high] == ' ':
  164. setLen(em.content, em.content.len-1)
  165. wr("\L")
  166. for i in 2..tok.line - em.lastLineNumber: wr("\L")
  167. em.col = 0
  168. for i in 1..em.indentLevel:
  169. wr(" ")
  170. em.fixedUntil = em.content.high
  171. var lastTokWasTerse = false
  172. case tok.tokType
  173. of tokKeywordLow..tokKeywordHigh:
  174. if endsInAlpha(em):
  175. wr(" ")
  176. elif not em.inquote and not endsInWhite(em) and
  177. em.lastTok notin openPars and not em.lastTokWasTerse:
  178. #and tok.tokType in oprSet
  179. wr(" ")
  180. if not em.inquote:
  181. wr(TokTypeToStr[tok.tokType])
  182. case tok.tokType
  183. of tkAnd: rememberSplit(splitAnd)
  184. of tkOr: rememberSplit(splitOr)
  185. of tkIn, tkNotin:
  186. rememberSplit(splitIn)
  187. wr(" ")
  188. else: discard
  189. else:
  190. # keywords in backticks are not normalized:
  191. wr(tok.ident.s)
  192. of tkColon:
  193. wr(TokTypeToStr[tok.tokType])
  194. wr(" ")
  195. of tkSemicolon, tkComma:
  196. wr(TokTypeToStr[tok.tokType])
  197. rememberSplit(splitComma)
  198. wr(" ")
  199. of tkParDotLe, tkParLe, tkBracketDotLe, tkBracketLe,
  200. tkCurlyLe, tkCurlyDotLe, tkBracketLeColon:
  201. if tok.strongSpaceA > 0 and not em.endsInWhite:
  202. wr(" ")
  203. wr(TokTypeToStr[tok.tokType])
  204. rememberSplit(splitParLe)
  205. of tkParRi,
  206. tkBracketRi, tkCurlyRi,
  207. tkBracketDotRi,
  208. tkCurlyDotRi,
  209. tkParDotRi,
  210. tkColonColon:
  211. wr(TokTypeToStr[tok.tokType])
  212. of tkDot:
  213. lastTokWasTerse = true
  214. wr(TokTypeToStr[tok.tokType])
  215. of tkEquals:
  216. if not em.inquote and not em.endsInWhite: wr(" ")
  217. wr(TokTypeToStr[tok.tokType])
  218. if not em.inquote: wr(" ")
  219. of tkOpr, tkDotDot:
  220. if tok.strongSpaceA == 0 and tok.strongSpaceB == 0:
  221. # bug #9504: remember to not spacify a keyword:
  222. lastTokWasTerse = true
  223. # if not surrounded by whitespace, don't produce any whitespace either:
  224. wr(tok.ident.s)
  225. else:
  226. if not em.endsInWhite: wr(" ")
  227. wr(tok.ident.s)
  228. template isUnary(tok): bool =
  229. tok.strongSpaceB == 0 and tok.strongSpaceA > 0
  230. if not isUnary(tok):
  231. rememberSplit(splitBinary)
  232. wr(" ")
  233. of tkAccent:
  234. if not em.inquote and endsInAlpha(em): wr(" ")
  235. wr(TokTypeToStr[tok.tokType])
  236. em.inquote = not em.inquote
  237. of tkComment:
  238. if not preventComment:
  239. emitComment(em, tok)
  240. of tkIntLit..tkStrLit, tkRStrLit, tkTripleStrLit, tkGStrLit, tkGTripleStrLit, tkCharLit:
  241. let lit = fileSection(em.config, em.fid, tok.offsetA, tok.offsetB)
  242. softLinebreak(em, lit)
  243. if endsInAlpha(em) and tok.tokType notin {tkGStrLit, tkGTripleStrLit}: wr(" ")
  244. em.lineSpan = countNewlines(lit)
  245. if em.lineSpan > 0: calcCol(em, lit)
  246. wr lit
  247. of tkEof: discard
  248. else:
  249. let lit = if tok.ident != nil: tok.ident.s else: tok.literal
  250. softLinebreak(em, lit)
  251. if endsInAlpha(em): wr(" ")
  252. wr lit
  253. em.lastTok = tok.tokType
  254. em.lastTokWasTerse = lastTokWasTerse
  255. em.lastLineNumber = tok.line + em.lineSpan
  256. em.lineSpan = 0
  257. proc starWasExportMarker*(em: var Emitter) =
  258. if em.content.endsWith(" * "):
  259. setLen(em.content, em.content.len-3)
  260. em.content.add("*")
  261. dec em.col, 2
  262. proc commaWasSemicolon*(em: var Emitter) =
  263. if em.semicolons == detectSemicolonKind:
  264. em.semicolons = if em.content.endsWith(", "): dontTouch else: useSemicolon
  265. if em.semicolons == useSemicolon and em.content.endsWith(", "):
  266. setLen(em.content, em.content.len-2)
  267. em.content.add("; ")
  268. proc curlyRiWasPragma*(em: var Emitter) =
  269. if em.content.endsWith("}"):
  270. setLen(em.content, em.content.len-1)
  271. em.content.add(".}")