Codly: full width code box with hanging line numbers

I’m starting to replace my ad-hoc solutions to code display with a systematic use of Codly. I’m quite happy with the package so far, but there’s one thing I haven’t been able to reproduce: a code box in full page width with hanging line numbers:

Any hint on how to tweak Codly’s parameters to achieve this?

Usually this can be achieved using raw.line, but since you’re unifying your code for use of codly alone, I’m only shifting focus there as well.

From its GitHub, it’s evident that the feature came about from Numbers outside margin by pmudry · Pull Request #60 · Dherse/codly · GitHub. At the moment, this can be found in the example 2.32.1, see for yourself:

#codly(number-placement: "outside"):

Thanks, but I’m not quite following. I am using the “outside” placement. My issue is to make the box as wide as the line width, with the number sticking in the margin.

Sorry, I couldn’t tell how familiar you are with the package.

I’m afraid the existing codly features alone don’t support this styling. For example, moving numbers to the left via number-format: (number) => place(dx: -0.5em, str(number)) hides them behind a white block, which is obviously not desired here, but also the code block doesn’t compensate for that space difference.

So I suggest you file a feature request in codly repository or apply your own changes by copying package files for the time being. Hopefully I overlooked something and I’m simply wrong.

Hi. It looks like you’d have to trade one ad-hoc solution for another, to make it work with Codly.

#import "@preview/codly:1.3.0": *

#set par(justify: true)
#show: codly-init
#codly(
  number-align: right,
  number-placement: "outside",
  zebra-fill: none,
  stroke: stroke(0pt),
  radius: 0pt,
)

#show raw.where(block: true): it => context {
  let stroke = 1pt
  let inset = 0.32em
  let magic-padding = 3.87pt
  let max-line-number = calc.max(..it.lines.map(it => it.number))
  let max-line-number-width = measure(raw(str(max-line-number))).width
  let offset = max-line-number-width + inset.to-absolute() * 2 + magic-padding
  // show grid.cell.where(x: 0): it => context panic(measure(it))
  // let offset = 13.75pt
  set block(stroke: stroke)
  show grid: set block(stroke: 0pt)
  show: move.with(dx: -offset)
  it
}

#lorem(50)
#raw(block: true, range(10).map(_ => lorem(10)).join("\n"))
#lorem(50)
#raw(block: true, range(3).map(_ => lorem(10)).join("\n"))
#lorem(50)

I tried figuring out the exact width, but I don’t know where the magic-padding length comes from. And the stroke kinda magically works, without move the stroke disappears. Thankfully, it doesn’t need any width manipulation. So it’s a mess, but so far looks promising. Only a real use case can show if it’s robust enough to not care much about the existence of this hack. A native feature would be better anyway.

So far, it doesn’t work with zebra-fill, because it still fills only inner blocks, but not the stroked one.

2 Likes

This works beautifully! (and I don’t care about zebra, which is the first thing I turn off). I’m going to try it on a “real use” and see how it goes…

It doesn’t seem to do well with page breaking (which I rarely need), but if there’s a way to add that, that’d be great.

Thanks!

1 Like

Now it makes sense why I wasn’t able to split long listing into pages. Because of the same move. Since you can’t move…well, you can move individual cells instead of rows, but the text still being clipped, so it doesn’t work. I wonder if it’s because of Codly, because that is a weird clipping issue, I don’t think I’ve noticed it with native Typst elements.

I’m pretty sure you can partially automate splitting by calling split-it(line-count-1st-page, line-count-full-page, code) and then splitting the code into multiple code blocks.

What is split-it?

This is the hack I used before, based on something I lifted somewhere from this forum:

#show raw.where(block: true): it => {
  set par(justify: false)
  let i = 0
  grid(
    columns: (100%, 100%),
    column-gutter: -100%,
    block(width: 100%, for line in it.text.split("\n") {
      i += 1
      box(width: 0pt, align(right, text(size: 0.9em, fill: luma(127), str(i) + h(1.5em))))
      hide(line)
      linebreak()
    }),
    it,
  )
}

#block(stroke: 0.5pt, inset: 5pt)[
  ```java
  final class ThisIsASingleLineProgam {}
  ```
]

It does the job (including page splitting and correct numbering of lines that wrap) but feels very hacky.
I started using codly because it gives me few add-ons that I like, such as displaying the language and highlighting parts of the code.

A custom function that implements the described behavior.

Looks like text is being clipped with this approach, too, with Codly.

What’s being clipped? It looks ok to me.

#show raw.where(block: true): it => {
  set text(0.9em)
  set par(justify: false)
  let i = 0
  block(stroke: 0.5pt, inset: 5pt, grid(
    columns: (100%, 100%),
    column-gutter: -100%,

    block(width: 100%, for line in it.text.split("\n") {
      i += 1
      box(width: 0pt, align(right, text(size: 0.9em, fill: luma(127), str(i) + h(1.5em))))
      hide(line)
      linebreak()
    }),
    it,
  ))
}

#set par(justify: true)
#lorem(50)

```java
public static void main (String[] args) {
  System.out.println(2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2);
  System.out.println(2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2);
  System.out.println(2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2);
}
```

#lorem(50)

You don’t use Codly, I said it’s clipped with Codly, same as other approach with Codly.

Got it.

I added that line in my setup to display the language:

if (i == 1) { h(1fr) + box(stroke: 0.5pt, outset: 2pt, fill: gray.lighten(50%), it.lang) }

No codly (and no highlighting), but that might be good enough for my needs (with very little code to maintain).

Thanks again for your help.

1 Like

It seems like the hack is overcomplicated. Just a couple of placed content would be enough:

#show raw.where(block: true): set block(width: 100%)
#show raw.where(block: true): it => {
  show: block.with(stroke: 0.5pt, inset: 5pt)
  place(for n in range(1, it.lines.len() + 1) {
    {
      set text(size: 0.9em, fill: luma(127))
      box(width: 0pt, align(right)[#n#h(1.5em)])
    }
    linebreak()
  })
  place(right, box(
    stroke: 0.5pt,
    outset: 2pt,
    fill: gray.lighten(50%),
    it.lang,
  ))
  it
}

#lorem(20)

```java
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
```

#lorem(20)

```java
final class ThisIsASingleLineProgam {}class ThisIsASingleLineProgam {}class ThisIsASingleLineProgam {}
final class ThisIsASingleLineProgam {}
```

#lorem(20)

Though very long lines will break the convenience of the linebreak() hack.

The problem, as you say, is that any long line will mess up the line numbers.

Here’s what I had settled on:

#show raw.where(block: true): it => {
  if it.lang == none { it } else {
    set text(0.9em)
    if not it.lang.starts-with(regex("[A-Z]")) { it } else {
      let num = if it.has("label") and it.label == <no_num> { _ => "" } else { str }
      let i = 0
      grid(
        columns: (100%, 100%),
        column-gutter: -100%,
        block(width: 100%, stroke: 0.5pt, outset: 5pt, for line in it.text.split("\n") {
          i += 1
          box(width: 0pt, align(right, text(size: 0.9em, fill: luma(127), num(i) + h(1.5em))))
          hide(line)
          if i == 1 { h(1fr) + box(stroke: 0.5pt, outset: 2pt, fill: luma(220), it.lang) }
          linebreak()
        }),
        it,
      )
    }
  }
}

The idea is to use java to get the standard layout and Java to bring in the language name and line numbers (which can be turned off with <no_num>).
I can try to simplify it a bit using place and lines.