layouter.nim 9.3 KB

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