layouter.nim 9.3 KB

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