Place the added open from the syntax tree, not from a line of text - #20500
Place the added open from the syntax tree, not from a line of text#20500xperiandri wants to merge 3 commits into
Conversation
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
Warning No PR link found in some release notes, please consider adding it.
|
What this looked like before, and what it does nowThree shapes, each with the 1. An attribute on the module's own line[<AutoOpen>] module Ns.Helpers
let write () = Console.WriteLine "hi"
open System // ← before: above the module declaration
[<AutoOpen>] module Ns.Helpers
let write () = Console.WriteLine "hi"[<AutoOpen>] module Ns.Helpers
open System // ← after: inside the module
let write () = Console.WriteLine "hi"2. No blank line under the headermodule Ns.Helpers
let write () = Console.WriteLine "hi"The arithmetic added two lines to reach "past the blank line under the header". With no blank line there, that landed past the first declaration, so the module Ns.Helpers
let write () = Console.WriteLine "hi" // ← before: still FS0039, the open is below it
open Systemmodule Ns.Helpers
open System // ← after
let write () = Console.WriteLine "hi"3.
|
|
🔍 Tooling Safety Check — Affects-Design-Time
|
T-Gro
left a comment
There was a problem hiding this comment.
🤖 🕵️ AI review — verify independently.
| | SynModuleOrNamespaceLeadingKeyword.Module keyword | ||
| | SynModuleOrNamespaceLeadingKeyword.Namespace keyword -> headerEndLine keyword ident None | ||
| // An implicit module has no header, so its first declaration opens the scope. | ||
| | SynModuleOrNamespaceLeadingKeyword.None -> range.StartLine - 1 |
There was a problem hiding this comment.
🤖 🕵️ Adding open System.Text puts it inside the block comment, leaving StringBuilder unresolved (FS0039).
(* header
*) let x = StringBuilder()There was a problem hiding this comment.
Correct, and thank you — verified with fsc rather than by reading:
t_new.fs(6,12): error FS0039: The value or constructor 'StringBuilder' is not defined.
Worth adding that main is no better here, it just fails on the neighbouring shape. Where the comment closes on its own line it wrote the open inside it:
(* header
open System.Text
*)
let x = StringBuilder()So this is one defect — the change is applied at the start of the anchor's line without regard for what is already on that line — which this PR happened to fix in one shape and inherit in the other.
Fixed in 21f852d. Whatever precedes the insertion point on its line is trivia the declaration follows, so the line is broken at the declaration instead:
(* header
*)
open System.Text
let x = StringBuilder()The declaration keeps its column on purpose — moved to the start of the line it would be offside of the open that the implicit module now begins with. The whitespace between *) and the declaration is taken into the change rather than left behind as trailing whitespace.
Covered by Fixes FS0039 for missing opens - declaration shares its line with the end of a comment.
The "open namespace" code fix and the completion that adds an open both decided where the declaration goes by recognizing a module header in the text of a line — `line.StartsWith "module" && not (line.EndsWith "=")` — followed by arithmetic that assumed a blank line under that header. Every shape the spelling missed put the open where it does not compile: `[<AutoOpen>] module Ns.Name` written on one line read as an implicit module and took the open above the module declaration, and a header with no blank line under it took the open below the first declaration, leaving the identifier as unresolved as before. InsertionContext.Pos now means one thing for every ScopeKind — the first line inside the scope — taken from the leading keyword the trivia records, the name it introduces and, for a nested module, the `=`. Attributes and doc comments sit above that line by construction, so no spelling has to be recognized. AdjustInsertionPoint keeps only the cosmetic step of stepping over the blank line under a header, and the editor's two insertion paths now share one implementation, which also follows the file's own line endings instead of the host's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The formatting-check CI job flagged AddOpenCodeFixProvider.fs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The insertion point is a position, and the change was applied at the start
of its line. That is the same thing whenever what precedes the declaration
on that line is whitespace, and something else entirely when it is not:
(* header
*) let x = StringBuilder()
Here the scope's first declaration is on the line that closes the comment,
so the open went between `(* header` and `*)`, inside the comment, leaving
the name as unresolved as it was. Whether the old code got this right was
luck of the same kind: it snapped an implicit module to line 1 and so wrote
above the comment here, but wrote inside it when the comment closed on a
line of its own.
What precedes the declaration is trivia that has to stay where it is, so
the line is broken at the declaration instead, and the declaration keeps
its column - moving it to the start of the line would put the open the
implicit module now begins with offside of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
21f852d to
ca89ff8
Compare
The "open namespace" code fix and the completion that adds an
openboth decided where the declaration goes by recognizing a module header in the text of a line —line.StartsWith "module" && not (line.EndsWith "=")— followed by arithmetic that assumed a blank line under that header.Every shape the spelling missed put the
openwhere it does not compile:[<AutoOpen>] module Ns.Namewritten on one line read as an implicit module, and theopenlanded above the module declaration.openbelow the first declaration, leaving the identifier as unresolved as before.namespace Nswith no blank line under it had theopenspliced between atypeheader and its first member.InsertionContext.Posnow means one thing for everyScopeKind— the first line inside the scope — taken from the leading keyword the trivia records, the name it introduces and, for a nested module, the=. Attributes and doc comments sit above that line by construction, so no spelling has to be recognized.ParsedInput.AdjustInsertionPointkeeps only the cosmetic step of stepping over the blank line under a header.The editor's two insertion paths (the code fix and the completion commit) now share one implementation, which also follows the file's own line endings instead of the host's.
Tests
OpenDeclarationInsertionTestsgains cases for the attribute-on-the-header-line and no-blank-line shapes;AddOpenOnTopOnTests/AddOpenOnTopOffTestsgain the end-to-end equivalents. One existing expectation moved by a line: theopennow sits next to the code rather than in the gap under the header.🤖 Generated with Claude Code