Skip to content

Improve signatures in tooltips - #1336

Merged
nojaf merged 5 commits into
fsprojects:mainfrom
nojaf:improve-codesnippets
Sep 18, 2026
Merged

nojaf merged 5 commits into
fsprojects:mainfrom
nojaf:improve-codesnippets

Conversation

@nojaf

@nojaf nojaf commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Before:

image

After:

image

A snippet is a <div class="fsdocs-snippet"> holding one <pre> for the line
numbers and one <pre> for the code, laid out with CSS grid instead of a
table. Markdown code blocks, notebook output and non-F# snippets share the
wrapper, so one selector reaches every code block on a page, the code column
scrolls on its own, and the line numbers stay out of anything the reader
selects and copies. A stylesheet or script that targets table.pre, td.lines,
td.snippet or .code-block-wrapper needs updating.

The default stylesheet ships GitHub's light and dark palettes. Tokens are
grouped into roles rather than given a hue each, and every color is a
--code-* custom property, so a site can restyle the scheme without touching
a selector.

Several token classes had no matching rule and rendered as plain text: union
cases, preprocessor directives, escaped characters, enumeration cases,
mutable bindings, and identifiers in every non-F# language, which is the bulk
of a C#, JavaScript or HTML snippet. The stylesheet was matching u, prep, e
and v, names the formatters no longer emit. It now follows Constants.CSS, and
compiler errors and omitted code are styled for the first time.

Tool tips carry those same classes. The compiler hands a signature over as
runs it has already classified and ToolTipReader was flattening them into one
string, so a tip rendered as undifferentiated text. ToolTipSpan gained a Token
case for a classified run and stripParameterAttributes works on those runs
instead of a string, which is a breaking change for anyone matching on
ToolTipSpan.

Three further defects surfaced in the tips along the way. Line breaks were
lost, because the compiler puts them inside a Text run with the next line's
indentation attached and only the Space case was split on, so a multi-line
signature collapsed onto one line. Doc comments arrived as the raw XML they
were written as, leaving summary and param tags in the tip; they are read as
XML now, and a see cref keeps the name it points at. The doc comment also now
reads before the signature rather than after it.

Fixes a code block rendered from an OtherBlock that opened its wrapper element
without ever closing it, leaving unbalanced HTML whenever snippet wrapping was
on.
The compiler tags a type abbreviation `Alias` rather than tagging it as
whatever it abbreviates, and ToolTipReader was grouping that with Parameter
and Local under TokenKind.Identifier, which the palette renders as plain
text. So `int` and `bool` were colored as types while `string`, `option`,
`list`, `array` and `unit` were not, in the same position of the same
signature. An alias is a type, so it now maps to ReferenceType.

This is what made a tool tip read as one flat color: in the tip for
CodeFormatter.ParseAndCheckSource only 5 of 34 runs carried a color, and the
abbreviations account for most of the rest.

UnknownEntity is the qualifier of a fully written name, the `System.` in
`System.DateTime`, so it takes the module color instead of plain.

Modules and namespaces move off the type color onto the green Primer reserves
for tags, which was the one hue in the GitHub palette we were not using.
Module access is everywhere in F# and `List.map String.trim` was a single
orange run; it now alternates container and function. Type parameters stay
with the types, since a leading apostrophe already marks them.
A tool tip is flowing text, so a signature wider than the tip wrapped
wherever the line ran out, in the middle of a parameter as often as not.
Across the documentation site 112 of the 523 code lines in tips were over 80
columns, and a type's member list was where nearly all of them sat: a
`static member` with six optional parameters ran to 600 characters on one
line.

NiceSignaturePrint breaks such a line the way a formatter would: after the
name, after each parameter of a tuple, and after each arrow, with the
parameters indented under the name and the result under them. A line that
fits, or that the compiler cannot read a type out of, is left as it was.

The compiler's own parser decides where the breaks go. A tip line is always
`<prefix> <name>: <type>`, and while `union case X.Y: ...` and a qualified
`static member X.Y: ...` do not parse as they stand, the type after the
first top-level colon always does. Only that part is handed to the parser,
as a `val`, and the prefix and name are carried through untouched. The
ranges on the resulting SynType map straight back onto the tip's classified
runs, so a break only ever splits a run and every run keeps the token kind
it came in with. Parsing a fragment this way costs 0.04 ms a line.

A member shown indented under its type keeps that indentation, and its
continuation lines indent from it rather than from the margin.

The module ships with a signature file exposing `layout` alone. The fsproj
lost its byte order mark when the signature was listed in it.
@nojaf
nojaf merged commit dcb3650 into fsprojects:main Sep 18, 2026
4 checks passed
This was referenced Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant