Skip to content

Commit 65b6119

Browse files
authored
Merge branch 'main' into repo-assist/fix-issue-924-watch-root-override-c4e35a7b2fb520aa
2 parents b23aef8 + b32175f commit 65b6119

9 files changed

Lines changed: 247 additions & 4 deletions

File tree

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
## 22.0.0-alpha.2 - 2026-03-13
6+
57
### Added
68
* Add `--root` option to `fsdocs watch` to override the root URL for generated pages. Useful for serving docs via GitHub Codespaces, reverse proxies, or other remote hosting where `localhost` URLs are inaccessible. E.g. `fsdocs watch --root /` or `fsdocs watch --root https://example.com/docs/`. When not set, defaults to `http://localhost:<port>/` as before. [#924](https://github.com/fsprojects/FSharp.Formatting/issues/924)
79
* Fix `fsdocs watch` hot-reload WebSocket to connect using the page's actual host (`window.location.host`) instead of a hardcoded `localhost:<port>`, so hot-reload works correctly in GitHub Codespaces, behind reverse proxies, and over HTTPS. [#924](https://github.com/fsprojects/FSharp.Formatting/issues/924)
@@ -10,12 +12,18 @@
1012
* `fsdocs convert` now accepts the input file as a positional argument (e.g. `fsdocs convert notebook.ipynb -o notebook.html`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1113
* `fsdocs convert` infers the output format from the output file extension when `--outputformat` is not specified (e.g. `-o out.md` implies `--outputformat markdown`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
1214
* `fsdocs convert` now accepts `-o` as a shorthand for `--output`. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
15+
* Added full XML doc comments (`<summary>`, `<param>`) to `Literate.ParseAndCheckScriptFile` and `Literate.ParseScriptString` to match the documentation style of the other `Literate.Parse*` methods.
16+
17+
### Fixed
18+
* `Literate.ParseScriptString` and `Literate.ParsePynbString` used a hardcoded Windows path (`C:\script.fsx`) as the fallback script filename when neither `path` nor `rootInputFolder` is supplied. The fallback is now a simple platform-neutral `script.fsx`.
19+
* Add regression tests for cross-assembly tooltip resolution (issue [#1085](https://github.com/fsprojects/FSharp.Formatting/issues/1085)): verify that hover tooltips for types whose fields reference types from other assemblies show the correct type names (not `obj`) when `#r` paths resolve correctly.
1320

1421
### Changed
1522
* Tooltip elements (`div.fsdocs-tip`) now use the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) (Baseline 2024: Chrome 114+, Firefox 125+, Safari 17+). Tooltips are placed in the browser's top layer — no `z-index` needed, always above all other content. Fixes a positioning bug where tooltips appeared offset when the page was scrolled. The previous `display`-toggle fallback has been removed. Tooltips also fade in with a subtle animation. [#422](https://github.com/fsprojects/FSharp.Formatting/issues/422), [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
1623
* Generated code tokens no longer use inline `onmouseover`/`onmouseout` event handlers. Tooltips are now triggered via `data-fsdocs-tip` / `data-fsdocs-tip-unique` attributes and a delegated event listener in `fsdocs-tips.js`. The `popover` attribute is also added to API-doc tooltip divs so they use the same top-layer path. [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
1724
* Changed `range` fields in `MarkdownSpan` and `MarkdownParagraph` DU cases from `MarkdownRange option` to `MarkdownRange`, using `MarkdownRange.zero` as the default/placeholder value instead of `None`.
1825
* When no template is provided (e.g. `fsdocs convert` without `--template`), `fsdocs-tip` tooltip divs are no longer included in the output. Tooltips require JavaScript/CSS from a template to function, so omitting them produces cleaner raw output. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
26+
* Use [`scrollbar-gutter: stable`](https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter) (Baseline 2024) on scroll containers (`main`, `#fsdocs-main-menu`, mobile menu, search dialog) to reserve scrollbar space and prevent layout shifts when content changes height. Also adds the missing `overflow-y: auto` to `main` so pages that exceed the viewport height are independently scrollable. [#1087](https://github.com/fsprojects/FSharp.Formatting/issues/1087), [#1088](https://github.com/fsprojects/FSharp.Formatting/pull/1088)
1927

2028
## 22.0.0-alpha.1 - 2026-03-03
2129

docs/content/fsdocs-default.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ header {
239239
list-style: none;
240240
padding: var(--spacing-300);
241241
overflow-y: auto;
242+
scrollbar-gutter: stable;
242243
}
243244

244245
& input:checked + .menu {
@@ -310,6 +311,8 @@ aside {
310311

311312
main {
312313
height: calc(100dvh - var(--header-height));
314+
overflow-y: auto;
315+
scrollbar-gutter: stable;
313316
}
314317

315318
#content {
@@ -469,6 +472,7 @@ main {
469472
grid-row: var(--main-menu-grid-row);
470473
grid-column: var(--main-menu-grid-column);
471474
overflow-y: auto;
475+
scrollbar-gutter: stable;
472476
}
473477

474478
main {
@@ -1269,6 +1273,7 @@ dialog {
12691273

12701274
& ul {
12711275
overflow-y: auto;
1276+
scrollbar-gutter: stable;
12721277
max-height: calc(50vh - var(--spacing-700) - var(--spacing-700));
12731278
list-style: none;
12741279
padding: 0;

src/FSharp.Formatting.Literate/Literate.fs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@ type Literate private () =
9898

9999
doc.With(paragraphs = pars)
100100

101-
/// Parse F# Script file to LiterateDocument
101+
/// <summary>
102+
/// Parse an F# script file (<c>.fsx</c>) to a <see cref="T:FSharp.Formatting.Literate.LiterateDocument"/>,
103+
/// type-checking it via the F# Compiler Service.
104+
/// </summary>
105+
/// <param name="path">The path to the <c>.fsx</c> script file to parse.</param>
106+
/// <param name="fscOptions">Additional F# compiler options (e.g. extra <c>-r:</c> references).</param>
107+
/// <param name="definedSymbols">Conditional-compilation symbols to define.</param>
108+
/// <param name="references">A map of reference labels to URLs, used to resolve Markdown-style links.</param>
109+
/// <param name="fsiEvaluator">An optional FSI evaluator; when supplied, code snippets are executed and their output included.</param>
110+
/// <param name="parseOptions">Markdown parse options. Defaults to <see cref="F:FSharp.Formatting.Markdown.MarkdownParseOptions.AllowYamlFrontMatter"/>.</param>
111+
/// <param name="rootInputFolder">The root folder used to resolve relative paths within the script. Defaults to the directory containing <paramref name="path"/>.</param>
112+
/// <param name="onError">A callback invoked with each error/warning message encountered during parsing or type-checking. Defaults to <c>ignore</c>.</param>
102113
static member ParseAndCheckScriptFile
103114
(
104115
path: string,
@@ -120,7 +131,20 @@ type Literate private () =
120131
|> Transformations.formatCodeSnippets path ctx
121132
|> Transformations.evaluateCodeSnippets ctx
122133

123-
/// Parse string as F# Script to LiterateDocument
134+
/// <summary>
135+
/// Parse a string containing F# script source code (<c>.fsx</c> syntax) to a
136+
/// <see cref="T:FSharp.Formatting.Literate.LiterateDocument"/>,
137+
/// type-checking it via the F# Compiler Service.
138+
/// </summary>
139+
/// <param name="content">The F# script source code to parse.</param>
140+
/// <param name="path">An optional file path used in diagnostics and to resolve relative references. When not supplied, defaults to <c>script.fsx</c> in <paramref name="rootInputFolder"/> (or just <c>script.fsx</c>).</param>
141+
/// <param name="fscOptions">Additional F# compiler options (e.g. extra <c>-r:</c> references).</param>
142+
/// <param name="definedSymbols">Conditional-compilation symbols to define.</param>
143+
/// <param name="references">A map of reference labels to URLs, used to resolve Markdown-style links.</param>
144+
/// <param name="fsiEvaluator">An optional FSI evaluator; when supplied, code snippets are executed and their output included.</param>
145+
/// <param name="parseOptions">Markdown parse options. Defaults to <see cref="F:FSharp.Formatting.Markdown.MarkdownParseOptions.AllowYamlFrontMatter"/>.</param>
146+
/// <param name="rootInputFolder">The root folder used to resolve relative paths within the script.</param>
147+
/// <param name="onError">A callback invoked with each error/warning message encountered during parsing or type-checking. Defaults to <c>ignore</c>.</param>
124148
static member ParseScriptString
125149
(
126150
content,
@@ -141,7 +165,7 @@ type Literate private () =
141165
| Some s -> s
142166
| None ->
143167
match rootInputFolder with
144-
| None -> "C:\\script.fsx"
168+
| None -> "script.fsx"
145169
| Some r -> Path.Combine(r, "script.fsx")
146170

147171
ParseScript(parseOptions, ctx).ParseAndCheckScriptFile(filePath, content, rootInputFolder, onError)
@@ -242,7 +266,7 @@ type Literate private () =
242266
| Some s -> s
243267
| None ->
244268
match rootInputFolder with
245-
| None -> "C:\\script.fsx"
269+
| None -> "script.fsx"
246270
| Some r -> Path.Combine(r, "script.fsx")
247271

248272
let content = ParsePynb.pynbStringToFsx content

tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,152 @@ c.Method()
479479
// Attribute annotations should not appear in rendered tooltips
480480
tooltip |> shouldNotContainText "[&lt;Optional"
481481
tooltip |> shouldNotContainText "[&lt;DefaultParameterValue"
482+
483+
// --------------------------------------------------------------------------------------
484+
// Tests for cross-assembly type resolution in tooltips (issue #1085)
485+
// --------------------------------------------------------------------------------------
486+
487+
/// Find the directory where the currently executing test assembly lives.
488+
/// Both CrossAssemblyA.dll and CrossAssemblyB.dll are copied there as project references.
489+
let private testBinDir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)
490+
491+
/// Return the tooltip text for the first token named `tokenText` that carries a tip.
492+
let private tipForToken (tokenText: string) (snips: Snippet[]) =
493+
snips
494+
|> Seq.tryPick (fun (Snippet(_, lines)) ->
495+
lines
496+
|> Seq.tryPick (fun (Line(_, spans)) ->
497+
spans
498+
|> Seq.tryPick (function
499+
| TokenSpan.Token(_, t, Some tips) when t = tokenText -> Some tips
500+
| _ -> None)))
501+
502+
/// Render tooltip spans to a single concatenated string for easy assertion.
503+
let private renderTips (spans: ToolTipSpans) =
504+
spans
505+
|> List.map (function
506+
| Literal s -> s
507+
| Emphasis inner ->
508+
inner
509+
|> List.map (function
510+
| Literal s -> s
511+
| _ -> "")
512+
|> String.concat ""
513+
| HardLineBreak -> "\n")
514+
|> String.concat ""
515+
516+
[<Test>]
517+
let ``Cross-assembly DU field type is shown as actual type name not obj in tooltip`` () =
518+
// Reproduce issue #1085: `Subject.Account of Did` should show `Did`, not `obj`
519+
// Use forward slashes so the paths are valid F# string literal content on Windows too.
520+
let assemblyAPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll").Replace('\\', '/')
521+
let assemblyBPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll").Replace('\\', '/')
522+
523+
let source =
524+
$"""#r "{assemblyAPath}"
525+
#r "{assemblyBPath}"
526+
open CrossAssemblyA
527+
open CrossAssemblyB
528+
529+
let subject = Subject.Account (CrossAssemblyA.Did.create "test")
530+
"""
531+
532+
let snips, errors = CodeFormatter.ParseAndCheckSource("/somewhere/test.fsx", source.Trim(), None, None, ignore)
533+
534+
errors |> shouldEqual [||]
535+
536+
// Find the tooltip for the `Subject` identifier and check its text
537+
let subjectTip =
538+
snips
539+
|> tipForToken "Subject"
540+
|> Option.map renderTips
541+
|> Option.defaultValue ""
542+
543+
subjectTip |> shouldNotEqual ""
544+
545+
// The tooltip for Subject should show the actual DU case type, not `obj`
546+
subjectTip |> shouldNotContainText "obj"
547+
subjectTip |> shouldContainText "Did"
548+
549+
[<Test>]
550+
let ``Cross-assembly record field types are shown as actual type names not obj in tooltip`` () =
551+
// Reproduce issue #1085: `TeamMember` record fields referencing types from another
552+
// assembly should show the correct type names (`Did`, `DateTimeOffset option`)
553+
// rather than `obj`.
554+
// Use forward slashes so the paths are valid F# string literal content on Windows too.
555+
let assemblyAPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll").Replace('\\', '/')
556+
let assemblyBPath = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll").Replace('\\', '/')
557+
558+
let source =
559+
$"""#r "{assemblyAPath}"
560+
#r "{assemblyBPath}"
561+
open CrossAssemblyB
562+
563+
let m : TeamMember = Unchecked.defaultof<TeamMember>
564+
"""
565+
566+
let snips, errors = CodeFormatter.ParseAndCheckSource("/somewhere/test.fsx", source.Trim(), None, None, ignore)
567+
568+
errors |> shouldEqual [||]
569+
570+
let teamMemberTip =
571+
snips
572+
|> tipForToken "TeamMember"
573+
|> Option.map renderTips
574+
|> Option.defaultValue ""
575+
576+
teamMemberTip |> shouldNotEqual ""
577+
578+
// Record field types from a different assembly should not appear as `obj`
579+
teamMemberTip |> shouldContainText "Did"
580+
teamMemberTip |> shouldContainText "DateTimeOffset"
581+
582+
[<Test>]
583+
let ``Cross-assembly DU field types are correct when using relative hash-r references`` () =
584+
// Reproduce the user scenario from issue #1085: using relative #r paths where the
585+
// script file is in the same directory as the referenced assemblies.
586+
// Set up a temporary directory containing the DLLs and a script path pointing there.
587+
let tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "fsharp-formatting-issue1085")
588+
System.IO.Directory.CreateDirectory(tempDir) |> ignore
589+
590+
try
591+
// Copy the test assemblies to the temp directory so relative #r can resolve them.
592+
let srcA = System.IO.Path.Combine(testBinDir, "CrossAssemblyA.dll")
593+
let srcB = System.IO.Path.Combine(testBinDir, "CrossAssemblyB.dll")
594+
System.IO.File.Copy(srcA, System.IO.Path.Combine(tempDir, "CrossAssemblyA.dll"), overwrite = true)
595+
System.IO.File.Copy(srcB, System.IO.Path.Combine(tempDir, "CrossAssemblyB.dll"), overwrite = true)
596+
597+
// Script path is inside the temp dir; FCS uses this to resolve relative #r paths.
598+
let scriptPath = System.IO.Path.Combine(tempDir, "test.fsx")
599+
600+
let source =
601+
"""#r "CrossAssemblyA.dll"
602+
#r "CrossAssemblyB.dll"
603+
open CrossAssemblyA
604+
open CrossAssemblyB
605+
606+
let subject = Subject.Account (CrossAssemblyA.Did.create "test")
607+
"""
608+
609+
let snips, errors = CodeFormatter.ParseAndCheckSource(scriptPath, source.Trim(), None, None, ignore)
610+
611+
errors |> shouldEqual [||]
612+
613+
let subjectTip =
614+
snips
615+
|> tipForToken "Subject"
616+
|> Option.map renderTips
617+
|> Option.defaultValue ""
618+
619+
subjectTip |> shouldNotEqual ""
620+
621+
// With correctly-resolved relative paths the tooltip should show Did, not obj.
622+
subjectTip |> shouldNotContainText "of obj"
623+
subjectTip |> shouldContainText "Did"
624+
625+
finally
626+
// Clean up
627+
try
628+
System.IO.Directory.Delete(tempDir, recursive = true)
629+
with _ ->
630+
()

tests/FSharp.CodeFormat.Tests/FSharp.CodeFormat.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
</ItemGroup>
1111
<ItemGroup>
1212
<ProjectReference Include="..\..\src\FSharp.Formatting.CodeFormat\FSharp.Formatting.CodeFormat.fsproj" />
13+
<ProjectReference Include="files\CrossAssemblyA\CrossAssemblyA.fsproj" />
14+
<ProjectReference Include="files\CrossAssemblyB\CrossAssemblyB.fsproj" />
1315
</ItemGroup>
1416
<ItemGroup>
1517
<PackageReference Include="FSharp.Core" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="Library.fs" />
8+
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="FSharp.Core" />
11+
</ItemGroup>
12+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace CrossAssemblyA
2+
3+
/// A simple single-case discriminated union that is defined in Assembly A.
4+
/// It is referenced from Assembly B to test cross-assembly tooltip resolution (issue #1085).
5+
type Did = private Did of string
6+
7+
module Did =
8+
/// Construct a Did value.
9+
let create (s: string) = Did s
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.1</TargetFramework>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="Library.fs" />
8+
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="FSharp.Core" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<ProjectReference Include="..\CrossAssemblyA\CrossAssemblyA.fsproj" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace CrossAssemblyB
2+
3+
open CrossAssemblyA
4+
5+
/// A discriminated union in Assembly B whose Account case holds a Did value
6+
/// from Assembly A. Used to test cross-assembly tooltip rendering (issue #1085).
7+
[<RequireQualifiedAccess>]
8+
type Subject =
9+
| Account of Did
10+
| Record of string
11+
12+
/// A record in Assembly B whose fields mix types from Assembly A (Did),
13+
/// primitive types, and generic wrapper types from the BCL.
14+
/// Used to test cross-assembly tooltip rendering (issue #1085).
15+
type TeamMember =
16+
{ Did: Did
17+
Name: string
18+
Active: bool
19+
JoinedAt: System.DateTimeOffset option }

0 commit comments

Comments
 (0)