diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 17bcd329342..0780074fa5b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -14,6 +14,7 @@ * Checker: report Bind resolution failures at the bound expression ([PR #20747](https://github.com/dotnet/fsharp/pull/20747)) * A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) * Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587)) +* `ParsedInput.FindNearestPointToInsertOpenDeclaration` reports where an `open` belongs from the syntax tree: `InsertionContext.Pos` is the first line inside the scope for every `ScopeKind`, so an `open` no longer lands above a header written as `[] module Ns.Name` or below the first declaration when no blank line follows the header, and it stays out of a block comment that runs on from the header. `ParsedInput.AdjustInsertionPoint` only skips the blank line under a header. ([PR #20500](https://github.com/dotnet/fsharp/pull/20500)) * Fix wrong results or non-terminating loops in optimized code when an integer `for` loop's start or end expression binds a local. ([Issue #20732](https://github.com/dotnet/fsharp/issues/20732), [PR #20733](https://github.com/dotnet/fsharp/pull/20733)) * Checker: resolve named arguments of failed and unfinished method calls ([PR #20749](https://github.com/dotnet/fsharp/pull/20749)) * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. Direct or mandatory-inline allocations are now reported at compile time as error FS3924; calls to non-inline allocation helpers, including handler-local helpers, remain legal and retain their method boundary during inlining and let-binding elimination. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295), [PR #20491](https://github.com/dotnet/fsharp/pull/20491)) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 2357fbcdff4..0b068496ae5 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -12,6 +12,7 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128), [PR #20695](https://github.com/dotnet/fsharp/pull/20695)) +* The "open namespace" code fix and the completion that adds an `open` place it from the syntax tree, so it no longer lands above the file's own module declaration (`[] module Ns.Name` on one line) or below the first declaration when no blank line follows the header, or inside a block comment that runs on from the header, and both follow the file's line endings. ([PR #20500](https://github.com/dotnet/fsharp/pull/20500)) * Fix record-type Rename corrupting copy-and-update expressions, including from abstract signatures, while retaining Find All References results. ([Issue #20791](https://github.com/dotnet/fsharp/issues/20791), [PR #20797](https://github.com/dotnet/fsharp/pull/20797)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) diff --git a/src/Compiler/Service/ServiceParsedInputOps.fs b/src/Compiler/Service/ServiceParsedInputOps.fs index cd68e7eb608..3d0bdf277f4 100644 --- a/src/Compiler/Service/ServiceParsedInputOps.fs +++ b/src/Compiler/Service/ServiceParsedInputOps.fs @@ -2434,12 +2434,23 @@ module ParsedInput = Kind: ScopeKind } - let tryFindNearestPointAndModules currentLine (ast: ParsedInput) (insertionPoint: OpenStatementInsertionPoint) = + let tryFindNearestPointAndModules (currentPos: pos) (ast: ParsedInput) (insertionPoint: OpenStatementInsertionPoint) = // We ignore all diagnostics during this operation // // Based on an initial review, no diagnostics should be generated. However the code should be checked more closely. use _ignoreAllDiagnostics = new DiagnosticsScope(false) + let currentLine = currentPos.Line + + let blockComments = + match ast with + | ParsedInput.ImplFile input -> + input.Trivia.CodeComments + |> List.choose (function + | CommentTrivia.BlockComment range -> Some range + | CommentTrivia.LineComment _ -> None) + | ParsedInput.SigFile _ -> [] + let mutable result = None let mutable ns = None let modules = ResizeArray() @@ -2487,9 +2498,9 @@ module ParsedInput = ) | _ -> () - let getMinColumn decls = + let tryGetFirstDeclStart decls = match decls with - | [] -> None + | [] -> ValueNone | firstDecl :: _ -> match firstDecl with | SynModuleDecl.NestedModule(range = r) @@ -2498,9 +2509,33 @@ module ParsedInput = | SynModuleDecl.Types(range = r) | SynModuleDecl.Exception(range = r) | SynModuleDecl.Open(range = r) - | SynModuleDecl.HashDirective(range = r) -> Some r - | _ -> None - |> Option.map (fun r -> r.StartColumn) + | SynModuleDecl.HashDirective(range = r) -> ValueSome r.Start + | _ -> ValueNone + + let rec skipTrailingBlockComments (firstDeclStart: pos voption) line = + match + blockComments + |> List.tryFind (fun comment -> + comment.StartLine <= line + && comment.EndLine > line + && firstDeclStart |> ValueOption.forall (posLt comment.Start)) + with + | Some comment -> skipTrailingBlockComments firstDeclStart comment.EndLine + | None -> line + + let headerEndLine (keyword: range) (ident: LongIdent) (equals: range option) firstDeclStart = + let nameEndLine = + ident + |> List.fold (fun line part -> max line part.idRange.EndLine) keyword.EndLine + + equals + |> Option.fold (fun line equalsSign -> max line equalsSign.EndLine) nameEndLine + |> skipTrailingBlockComments firstDeclStart + + let bodyAnchor headerLine (firstDeclStart: pos voption) defaultColumn = + match firstDeclStart with + | ValueSome start when start.Line = headerLine -> struct (headerLine - 1, start.Column) + | _ -> struct (headerLine, defaultColumn) let rec walkImplFileInput (file: ParsedImplFileInput) = List.iter (walkSynModuleOrNamespace []) file.Contents @@ -2522,14 +2557,16 @@ module ParsedInput = let fullIdent = parent @ ident - // Use trivia to get the actual module/namespace keyword line, which excludes attributes - let startLine = + let firstDeclStart = tryGetFirstDeclStart decls + + let headerLine = match trivia.LeadingKeyword with - | SynModuleOrNamespaceLeadingKeyword.Module moduleRange -> moduleRange.StartLine - | SynModuleOrNamespaceLeadingKeyword.Namespace namespaceRange -> namespaceRange.StartLine - 1 - | SynModuleOrNamespaceLeadingKeyword.None -> - // No keyword (implicit module), use range.StartLine - if isModule then range.StartLine else range.StartLine - 1 + | SynModuleOrNamespaceLeadingKeyword.Module keyword + | SynModuleOrNamespaceLeadingKeyword.Namespace keyword -> headerEndLine keyword ident None firstDeclStart + | SynModuleOrNamespaceLeadingKeyword.None -> range.StartLine - 1 + + let struct (lineAboveBody, column) = + bodyAnchor headerLine firstDeclStart range.StartColumn let scopeKind = match isModule, parent with @@ -2537,7 +2574,7 @@ module ParsedInput = | true, _ -> NestedModule | _ -> Namespace - doRange scopeKind fullIdent startLine range.StartColumn + doRange scopeKind fullIdent lineAboveBody column addModule (fullIdent, range) List.iter (walkSynModuleDecl fullIdent) decls @@ -2545,22 +2582,36 @@ module ParsedInput = match decl with | SynModuleDecl.NamespaceFragment fragment -> walkSynModuleOrNamespace parent fragment | SynModuleDecl.NestedModule(moduleInfo = compInfo; decls = decls; range = range; trivia = trivia) -> + let (SynComponentInfo(attributes = attributes)) = compInfo let ident = compInfo.LongIdent let fullIdent = parent @ ident addModule (fullIdent, range) if range.EndLine >= currentLine then - // Use trivia to get the actual module keyword line, which excludes attributes - let moduleKeywordLine = + let firstDeclStart = tryGetFirstDeclStart decls + + let headerLine = match trivia.ModuleKeyword with - | Some moduleKeywordRange -> moduleKeywordRange.StartLine + | Some moduleKeyword -> headerEndLine moduleKeyword ident trivia.EqualsRange firstDeclStart | None -> range.StartLine // Fallback if trivia unavailable let moduleBodyIndentation = - getMinColumn decls |> Option.defaultValue (range.StartColumn + 4) - - doRange NestedModule fullIdent moduleKeywordLine moduleBodyIndentation - List.iter (walkSynModuleDecl fullIdent) decls + firstDeclStart + |> ValueOption.map _.Column + |> ValueOption.defaultValue (range.StartColumn + 4) + + let struct (lineAboveBody, bodyColumn) = + bodyAnchor headerLine firstDeclStart moduleBodyIndentation + + let useMayBeInModuleAttribute = + firstDeclStart + |> ValueOption.exists (fun start -> start.Line = headerLine && posLt currentPos start) + && attributes + |> List.exists (fun attributeList -> attributeList.Range.EndLine = headerLine) + + if headerLine <= currentLine && not useMayBeInModuleAttribute then + doRange NestedModule fullIdent lineAboveBody bodyColumn + List.iter (walkSynModuleDecl fullIdent) decls | SynModuleDecl.Open(_, range) -> doRange OpenDeclaration [] range.EndLine (range.StartColumn - 5) | SynModuleDecl.HashDirective(_, range) -> doRange HashDirective [] range.EndLine range.StartColumn | _ -> () @@ -2604,14 +2655,14 @@ module ParsedInput = Pos = mkPos (Line.fromZ m.Range.EndLine) m.Range.StartColumn }) - let TryFindInsertionContext - (currentLine: int) + let TryFindInsertionContextAt + (currentPos: pos) (parsedInput: ParsedInput) (partiallyQualifiedName: MaybeUnresolvedIdent[]) (insertionPoint: OpenStatementInsertionPoint) = let res, modules = - tryFindNearestPointAndModules currentLine parsedInput insertionPoint + tryFindNearestPointAndModules currentPos parsedInput insertionPoint let res = res @@ -2658,59 +2709,29 @@ module ParsedInput = match find () with | struct (_, context) -> e, context) - /// Corrects insertion line number based on kind of scope and text surrounding the insertion point. - let AdjustInsertionPoint (getLineStr: int -> string) ctx = - let line = - match ctx.ScopeKind with - | ScopeKind.TopModule -> - if ctx.Pos.Line > 1 then - // it's an implicit module without any open declarations - let line = getLineStr (ctx.Pos.Line - 2) - - let isImplicitTopLevelModule = - not (line.StartsWithOrdinal("module") && not (line.EndsWithOrdinal("="))) - - if isImplicitTopLevelModule then 1 else ctx.Pos.Line - else - 1 - - | ScopeKind.Namespace -> - // For namespaces the start line is start line of the first nested entity - // If we are not on the first line, try to find opening namespace, and return line after it (in F# format) - if ctx.Pos.Line > 1 then - [ 0 .. ctx.Pos.Line - 1 ] - |> List.mapi (fun i line -> i, getLineStr line) - |> List.tryPick (fun (i, lineStr) -> - if lineStr.StartsWithOrdinal("namespace") then - Some i - else - None) - |> function - // move to the next line below "namespace" and convert it to F# 1-based line number - | Some line -> line + 2 - | None -> ctx.Pos.Line - // If we are on 1st line in the namespace ctx, this line _should_ be the namespace declaration, check it and return next line. - // Otherwise, return first line (which theoretically should not happen). - else - let lineStr = getLineStr (ctx.Pos.Line - 1) - - if lineStr.StartsWithOrdinal("namespace") then - ctx.Pos.Line + 1 - else - ctx.Pos.Line - - | _ -> ctx.Pos.Line - - mkPos line ctx.Pos.Column - - let FindNearestPointToInsertOpenDeclaration + let TryFindInsertionContext (currentLine: int) (parsedInput: ParsedInput) + (partiallyQualifiedName: MaybeUnresolvedIdent[]) + (insertionPoint: OpenStatementInsertionPoint) + = + TryFindInsertionContextAt (mkPos currentLine 0) parsedInput partiallyQualifiedName insertionPoint + + let AdjustInsertionPoint (getLineStr: int -> string) ctx = + match ctx.ScopeKind with + | ScopeKind.TopModule + | ScopeKind.Namespace + | ScopeKind.NestedModule when getLineStr (Line.toZ ctx.Pos.Line) = "" -> mkPos (ctx.Pos.Line + 1) ctx.Pos.Column + | _ -> ctx.Pos + + let FindNearestPointToInsertOpenDeclarationAt + (currentPos: pos) + (parsedInput: ParsedInput) (entity: ShortIdents) (insertionPoint: OpenStatementInsertionPoint) = let ctx = - match tryFindNearestPointAndModules currentLine parsedInput insertionPoint with + match tryFindNearestPointAndModules currentPos parsedInput insertionPoint with | Some(scope, _, point), modules -> let struct (_, context) = findBestPositionToInsertOpenDeclaration modules scope point entity @@ -2748,8 +2769,7 @@ module ParsedInput = | _ -> 0 if lastReferenceLine > 0 then - // `AdjustInsertionPoint` snaps a `TopModule` position up to line 1, above the directives; - // remap it to `HashDirective`, which (like the other scopes) it passes through unchanged. + // AdjustInsertionPoint would move a TopModule position below the blank line under the directives. let scopeKind = if ctx.ScopeKind = ScopeKind.TopModule then ScopeKind.HashDirective @@ -2763,3 +2783,11 @@ module ParsedInput = else ctx | _ -> ctx + + let FindNearestPointToInsertOpenDeclaration + (currentLine: int) + (parsedInput: ParsedInput) + (entity: ShortIdents) + (insertionPoint: OpenStatementInsertionPoint) + = + FindNearestPointToInsertOpenDeclarationAt (mkPos currentLine 0) parsedInput entity insertionPoint diff --git a/src/Compiler/Service/ServiceParsedInputOps.fsi b/src/Compiler/Service/ServiceParsedInputOps.fsi index ed83c88f5a9..3ed8718e79f 100644 --- a/src/Compiler/Service/ServiceParsedInputOps.fsi +++ b/src/Compiler/Service/ServiceParsedInputOps.fsi @@ -131,7 +131,8 @@ type public InsertionContext = /// Current scope kind. ScopeKind: ScopeKind - /// Current position (F# compiler line number). + /// Where the `open` belongs (F# compiler line number): the first line inside the scope, below its header, + /// opens and directives. For a nested module whose body starts on the header's line, that line. Pos: pos } @@ -198,6 +199,19 @@ module public ParsedInput = ShortIdents) -> (InsertionContextEntity * InsertionContext)[]) + /// Like `TryFindInsertionContext`, for a use at a position: on the line of a nested module declared in one line + /// that carries an attribute, only a use after the start of the module's body is inside the module. + val internal TryFindInsertionContextAt: + currentPos: pos -> + parsedInput: ParsedInput -> + partiallyQualifiedName: MaybeUnresolvedIdent[] -> + insertionPoint: OpenStatementInsertionPoint -> + (( (* requiresQualifiedAccessParent: *) ShortIdents option (* autoOpenParent: *) * + ShortIdents option (* entityNamespace *) * + ShortIdents option (* entity: *) * + ShortIdents) + -> (InsertionContextEntity * InsertionContext)[]) + /// Returns `InsertContext` based on current position and symbol idents. val FindNearestPointToInsertOpenDeclaration: currentLine: int -> @@ -206,10 +220,19 @@ module public ParsedInput = insertionPoint: OpenStatementInsertionPoint -> InsertionContext + /// Like `FindNearestPointToInsertOpenDeclaration`, for a use at a position. + val internal FindNearestPointToInsertOpenDeclarationAt: + currentPos: pos -> + parsedInput: ParsedInput -> + entity: ShortIdents -> + insertionPoint: OpenStatementInsertionPoint -> + InsertionContext + /// Returns long identifier at position. val GetLongIdentAt: parsedInput: ParsedInput -> pos: pos -> LongIdent option - /// Corrects insertion line number based on kind of scope and text surrounding the insertion point. + /// Moves the insertion point below the blank line that follows a declaration header. `getLineStr` returns + /// the trimmed text of a zero-based line, or an empty string past the end of the file. val AdjustInsertionPoint: getLineStr: (int -> string) -> ctx: InsertionContext -> pos [] diff --git a/tests/FSharp.Compiler.Service.Tests/ActivePatternCompletionTests.fs b/tests/FSharp.Compiler.Service.Tests/ActivePatternCompletionTests.fs index b7f00cffcd0..6acb6e3c5d5 100644 --- a/tests/FSharp.Compiler.Service.Tests/ActivePatternCompletionTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/ActivePatternCompletionTests.fs @@ -509,7 +509,7 @@ let ``referenced case names retain the namespace-level insertion scope`` (patter |> Assert.Single Assert.Equal(Some ns, entity.Namespace) Assert.Equal(qualifier, entity.Qualifier) - Assert.Equal((if atTop then Position.mkPos 1 0 else Position.mkPos 4 8), insertion.Pos) + Assert.Equal((if atTop then Position.mkPos 2 0 else Position.mkPos 4 8), insertion.Pos) let opened = if atTop then context.Source.Replace("namespace N\n", $"namespace N\nopen {entity.Namespace.Value}\n") diff --git a/tests/FSharp.Compiler.Service.Tests/OpenDeclarationInsertionTests.fs b/tests/FSharp.Compiler.Service.Tests/OpenDeclarationInsertionTests.fs index ef1f09f466b..886e0507ce3 100644 --- a/tests/FSharp.Compiler.Service.Tests/OpenDeclarationInsertionTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/OpenDeclarationInsertionTests.fs @@ -3,25 +3,17 @@ module FSharp.Compiler.Service.Tests.OpenDeclarationInsertionTests open Xunit open FSharp.Compiler.EditorServices open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Text /// Computes the position an `open` would be inserted at, mirroring the real editor consumer /// (RoslynHelpers.insertContext): `FindNearestPointToInsertOpenDeclaration` followed by /// `AdjustInsertionPoint`. Asserting on the adjusted position is what the user actually sees. -let private findOpenInsertion (insertionPoint: OpenStatementInsertionPoint) (fileName: string) (source: string) (entity: string) = +let private adjustedInsertion findNearest fileName (source: string) (entity: string) = let parseTree = parseSourceCode (fileName, source) let lines = source.Replace("\r\n", "\n").Split('\n') - let currentLine = - let mutable i = lines.Length - 1 - while i > 0 && System.String.IsNullOrWhiteSpace(lines.[i]) do - i <- i - 1 - i + 1 - - let idents = entity.Split('.') - - let ctx = - ParsedInput.FindNearestPointToInsertOpenDeclaration currentLine parseTree idents insertionPoint + let ctx = findNearest parseTree (entity.Split('.')) let getLineStr line = if line >= 0 && line < lines.Length then lines.[line].Trim() else "" @@ -29,6 +21,25 @@ let private findOpenInsertion (insertionPoint: OpenStatementInsertionPoint) (fil let pos = ParsedInput.AdjustInsertionPoint getLineStr ctx pos.Line, pos.Column +let private findOpenInsertionAt currentLine insertionPoint fileName source entity = + adjustedInsertion + (fun parseTree idents -> + ParsedInput.FindNearestPointToInsertOpenDeclaration currentLine parseTree idents insertionPoint) + fileName + source + entity + +let private findOpenInsertionFrom (usePos: pos) insertionPoint fileName source entity = + adjustedInsertion + (fun parseTree idents -> + ParsedInput.FindNearestPointToInsertOpenDeclarationAt usePos parseTree idents insertionPoint) + fileName + source + entity + +let private findOpenInsertion insertionPoint fileName (source: string) entity = + findOpenInsertionAt (source.TrimEnd().Split('\n').Length) insertionPoint fileName source entity + let private findOpenInsertionLine fileName source entity = findOpenInsertion OpenStatementInsertionPoint.TopLevel fileName source entity |> fst @@ -116,8 +127,85 @@ let ``Open not forced above named module in fsx`` () = let x = System.IO.File.ReadAllText "a" """ let line = findOpenInsertionLine "test.fsx" source "System.IO" - // Below the `module Foo` header (line 1), inside the module. - Assert.Equal(2, line) + Assert.Equal(3, line) + +[] +[] module Foo + +let x = System.IO.File.ReadAllText "a" +""", 3)>] +[] +[] +[] +[] +let ``Open goes below the header of a module or namespace, with or without a blank line`` (source: string, expectedLine: int) = + Assert.Equal(expectedLine, findOpenInsertionLine "test.fs" source "System.IO") + +[] +let ``Open goes in front of the declaration that follows a block comment ending the header`` () = + let source = """module Foo (* header +*) let x = System.IO.File.ReadAllText "a" +""" + Assert.Equal((2, 3), findOpenInsertion OpenStatementInsertionPoint.TopLevel "test.fs" source "System.IO") + +[] +let ``Open goes below every block comment that runs on from the header`` () = + let source = """module Top + +module M = (* first +*) (* second +*) + let x = System.IO.File.ReadAllText "a" +""" + Assert.Equal((6, 4), findOpenInsertion OpenStatementInsertionPoint.Nearest "test.fs" source "System.IO") + +[] +let ``Open placed at the first declaration of a nested module declared on one line`` () = + let source = """module Top + +module M = let x = System.IO.File.ReadAllText "a" +""" + let pos = findOpenInsertion OpenStatementInsertionPoint.Nearest "test.fs" source "System.IO" + Assert.Equal((3, 11), pos) + +[] +let ``Open ignores a block comment that starts inside the first declaration of a nested module`` () = + let source = """module Top +module M = let x = (* body + *) System.IO.File.ReadAllText "a" +""" + let pos = findOpenInsertion OpenStatementInsertionPoint.Nearest "test.fs" source "System.IO" + Assert.Equal((2, 11), pos) + +[] +[] +[] module M = let y = 1", 2)>] +[] module M = let y = 1", 3)>] +let ``Open stays out of a nested module declared on one line that the use is not in`` (nestedModule: string, useLine: int) = + let source = $"module Top\nlet x = Console.WriteLine 42\n{nestedModule}\n" + let pos = findOpenInsertionAt useLine OpenStatementInsertionPoint.Nearest "test.fs" source "System" + Assert.Equal((2, 0), pos) + +[] +[] +[] +let ``Open goes into an attributed nested module declared on one line only for a use in its body`` (useColumn: int, openColumn: int) = + let source = "module Top\n[] module M = let y = System.IO.File.ReadAllText \"a\"\n" + let usePos = Position.mkPos 2 useColumn + let pos = findOpenInsertionFrom usePos OpenStatementInsertionPoint.Nearest "test.fs" source "System.IO" + Assert.Equal((2, openColumn), pos) [] // Only #r/#load drive placement; other directives (#time/#help/#I/...) must not let ``Non reference directive does not drive placement in fsx`` () = diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs index 10b0db02ef3..f6cd69bc92b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs @@ -21,8 +21,6 @@ open CancellableTasks type internal AddOpenCodeFixProvider [] (assemblyContentProvider: AssemblyContentProvider) = inherit CodeFixProvider() - static let br = Environment.NewLine - let fixUnderscoresInMenuText (text: string) = text.Replace("_", "__") let qualifySymbolFix (context: CodeFixContext) (fullName, qualifier) = @@ -32,45 +30,10 @@ type internal AddOpenCodeFixProvider [] (assemblyContentPr Changes = [ TextChange(context.Span, qualifier) ] } - let getOpenDeclaration (sourceText: SourceText) (ctx: InsertionContext) (ns: string) = - // insertion context counts from 2, make the world sane - let insertionLineNumber = ctx.Pos.Line - 2 - let margin = String(' ', ctx.Pos.Column) - - let getLineStr line = - sourceText.Lines[line].ToString().Trim() - - let skipBlank line = - if getLineStr line = "" then line + 1 else line - - let startLineNumber, openDeclaration = - match ctx.ScopeKind with - | ScopeKind.TopModule -> - match getLineStr insertionLineNumber with - - // explicit top level module - | line when line.StartsWith "module" && not (line.EndsWith "=") -> - skipBlank (insertionLineNumber + 1), $"{margin}open {ns}{br}{br}" - - // nested module, shouldn't be here - | line when line.StartsWith "module" -> insertionLineNumber, $"{margin}open {ns}{br}{br}" - - // implicit top level module - | _ -> insertionLineNumber, $"{margin}open {ns}{br}{br}" - - | ScopeKind.Namespace - | ScopeKind.NestedModule -> - let pos = OpenDeclarationHelper.getInsertionPosition getLineStr ctx - skipBlank (Line.toZ pos.Line), $"{margin}open {ns}{br}{br}" - | ScopeKind.OpenDeclaration -> insertionLineNumber + 1, $"{margin}open {ns}{br}" - | ScopeKind.HashDirective -> insertionLineNumber + 1, $"open {ns}{br}{br}" - - let start = sourceText.Lines[startLineNumber].Start - TextChange(TextSpan(start, 0), openDeclaration) - let openNamespaceFix ctx name ns multipleNames sourceText = let displayText = $"open {ns}" + (if multipleNames then " (" + name + ")" else "") - let change = getOpenDeclaration sourceText ctx ns + + let change = OpenDeclarationHelper.getOpenDeclarationChange sourceText ctx ns { Name = CodeFix.AddOpen @@ -245,8 +208,8 @@ type internal AddOpenCodeFixProvider [] (assemblyContentPr OpenStatementInsertionPoint.Nearest let createEntity = - ParsedInput.TryFindInsertionContext - unresolvedIdentRange.StartLine + ParsedInput.TryFindInsertionContextAt + unresolvedIdentRange.Start parseResults.ParseTree maybeUnresolvedIdents insertionPoint diff --git a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs index b3c0e8e5765..27449e189a3 100644 --- a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs @@ -231,12 +231,53 @@ module internal RoslynHelpers = Async.Start(computation, cancellationToken) module internal OpenDeclarationHelper = - let getInsertionPosition (getLineStr: int -> string) (ctx: InsertionContext) = - match ctx.ScopeKind with - | ScopeKind.Namespace when (getLineStr (ctx.Pos.Line - 1)).StartsWith("namespace", StringComparison.Ordinal) -> - Position.mkPos (ctx.Pos.Line + 1) ctx.Pos.Column - | ScopeKind.Namespace -> ctx.Pos - | _ -> ParsedInput.AdjustInsertionPoint getLineStr ctx + let getOpenDeclarationChange (sourceText: SourceText) (ctx: InsertionContext) (ns: string) : TextChange = + let getLineStr line = + if line >= 0 && line < sourceText.Lines.Count then + sourceText.Lines[line].ToString().Trim() + else + "" + + let pos = ParsedInput.AdjustInsertionPoint getLineStr ctx + let line = sourceText.Lines[min (Line.toZ pos.Line) (sourceText.Lines.Count - 1)] + + let lineBreakOf (line: TextLine) = + sourceText.ToString(TextSpan.FromBounds(line.End, line.EndIncludingLineBreak)) + + let lineBreak = + match lineBreakOf line with + | "" when line.LineNumber > 0 -> lineBreakOf sourceText.Lines[line.LineNumber - 1] + | "" -> Environment.NewLine + | breakChars -> breakChars + + let blankLineAboveOpen = + match ctx.ScopeKind with + | ScopeKind.TopModule + | ScopeKind.Namespace + | ScopeKind.NestedModule when getLineStr (line.LineNumber - 1) <> "" -> lineBreak + | _ -> "" + + let blankLineBelowOpen = + if ctx.ScopeKind = ScopeKind.OpenDeclaration || getLineStr line.LineNumber = "" then + "" + else + lineBreak + + let margin = String(' ', pos.Column) + let column = min pos.Column (line.End - line.Start) + + let textBeforeDeclaration = + sourceText.ToString(TextSpan(line.Start, column)).TrimEnd() + + let openLine = margin + "open " + ns + lineBreak + + if textBeforeDeclaration.Length > 0 then + TextChange( + TextSpan(line.Start + textBeforeDeclaration.Length, column - textBeforeDeclaration.Length), + lineBreak + openLine + lineBreak + margin + ) + else + TextChange(TextSpan(line.Start, 0), blankLineAboveOpen + openLine + blankLineBelowOpen) /// /// Inserts open declaration into `SourceText`. @@ -245,58 +286,8 @@ module internal OpenDeclarationHelper = /// Insertion context. Typically returned from tryGetInsertionContext /// Namespace to open. let insertOpenDeclaration (sourceText: SourceText) (ctx: InsertionContext) (ns: string) : SourceText * int = - let mutable minPos = None - - let insert line lineStr (sourceText: SourceText) : SourceText = - let ln = sourceText.Lines.[line] - let pos = ln.Start - - minPos <- - match minPos with - | None -> Some pos - | Some oldPos -> Some(min oldPos pos) - - // find the line break characters on the previous line to use, Environment.NewLine should not be used - // as it makes assumptions on the line endings in the source. - let lineBreak = - ln.Text.ToString(TextSpan(ln.End, ln.EndIncludingLineBreak - ln.End)) - - sourceText.WithChanges(TextChange(TextSpan(pos, 0), lineStr + lineBreak)) - - let getLineStr line = - sourceText.Lines.[line].ToString().Trim() - - let pos = getInsertionPosition getLineStr ctx - - let docLine = Line.toZ pos.Line - let lineStr = (String.replicate pos.Column " ") + "open " + ns - - // If we're at the top of a file (e.g., F# script) then add a newline before adding the open declaration - let sourceText = - if docLine = 0 then - sourceText |> insert docLine Environment.NewLine |> insert docLine lineStr - else - sourceText |> insert docLine lineStr - - // if there's no a blank line between open declaration block and the rest of the code, we add one - let sourceText = - if sourceText.Lines.[docLine + 1].ToString().Trim() <> "" then - sourceText |> insert (docLine + 1) "" - else - sourceText - - let sourceText = - // for top level module we add a blank line between the module declaration and first open statement - if - (pos.Column = 0 || ctx.ScopeKind = ScopeKind.Namespace) - && docLine > 0 - && not (sourceText.Lines.[docLine - 1].ToString().Trim().StartsWith "open") - then - sourceText |> insert docLine "" - else - sourceText - - sourceText, minPos |> Option.defaultValue 0 + let change = getOpenDeclarationChange sourceText ctx ns + sourceText.WithChanges change, change.Span.Start // http://www.fssnip.net/7S3/title/Intersperse-a-list module List = diff --git a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs index 8e402c59132..85eea6c76fd 100644 --- a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs @@ -405,11 +405,13 @@ type internal FSharpCompletionProvider | true, keywordDescription -> Task.FromResult(CompletionDescription.FromText(keywordDescription)) | false, _ -> Task.FromResult(CompletionDescription.Empty) - override _.GetChangeAsync(document, item, _, cancellationToken) : Task = + static member GetChangeAsyncAux(document: Document, item: Completion.CompletionItem, insertionPoint: OpenStatementInsertionPoint) = cancellableTask { use _logBlock = Logger.LogBlockMessage document.Name LogEditorFunctionId.Completion_GetChangeAsync + let! cancellationToken = CancellableTask.getCancellationToken () + let fullName = match item.Properties.TryGetValue FullNamePropName with | true, x -> ValueSome x @@ -469,15 +471,9 @@ type internal FSharpCompletionProvider |> ValueOption.map (fun x -> x.Split '.') |> ValueOption.defaultValue [||] - let insertionPoint = - if settings.CodeFixes.AlwaysPlaceOpensAtTopLevel then - OpenStatementInsertionPoint.TopLevel - else - OpenStatementInsertionPoint.Nearest - let ctx = - ParsedInput.FindNearestPointToInsertOpenDeclaration - (Line.fromZ line.LineNumber) + ParsedInput.FindNearestPointToInsertOpenDeclarationAt + (Position.fromZ line.LineNumber (item.Span.Start - line.Start)) parseResults.ParseTree fullNameIdents insertionPoint @@ -494,4 +490,13 @@ type internal FSharpCompletionProvider return CompletionChange.Create(TextChange(fullChangingSpan, changedText)).WithNewPosition(Nullable(changedSpan.End)) } + + override _.GetChangeAsync(document, item, _, cancellationToken) : Task = + let insertionPoint = + if settings.CodeFixes.AlwaysPlaceOpensAtTopLevel then + OpenStatementInsertionPoint.TopLevel + else + OpenStatementInsertionPoint.Nearest + + FSharpCompletionProvider.GetChangeAsyncAux(document, item, insertionPoint) |> CancellableTask.start cancellationToken diff --git a/vsintegration/tests/FSharp.Editor.Tests/ActivePatternEditorTests.fs b/vsintegration/tests/FSharp.Editor.Tests/ActivePatternEditorTests.fs index 47dbc65c2ae..05f96254aad 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/ActivePatternEditorTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/ActivePatternEditorTests.fs @@ -169,7 +169,7 @@ let ``Add Open applied case uses exact target and placement`` (caseName: string, [", "Patterns", false, true, "Outer")>] [ n | _ -> 0", "Closed.Patterns", true, true, "Closed")>] [ n | _ -> 0", "Closed.Patterns", false, true, "Closed")>] -let ``Add Open supports adjacent headers without changing existing spacing`` +let ``Add Open supports adjacent headers and separates the open from them`` (body: string, ns: string, atTop: bool, sameFile: bool, container: string) = let code = @@ -187,15 +187,15 @@ let ``Add Open supports adjacent headers without changing existing spacing`` let expected = if container = "Closed" then - code.Replace("module Outer =\n", $"open {ns}\n\nmodule Outer =\n") + code.Replace("module Outer =\n", $"\nopen {ns}\n\nmodule Outer =\n") elif sameFile then let margin = if container = "" then "" else " " let anchor = $"{margin}module Use =\n" - code.Replace(anchor, $"{margin}open {ns}\n\n{anchor}") + code.Replace(anchor, $"\n{margin}open {ns}\n\n{anchor}") elif atTop then - code.Replace("namespace Consumer\n", $"namespace Consumer\nopen {ns}\n\n") + code.Replace("namespace Consumer\n", $"namespace Consumer\n\nopen {ns}\n\n") else - code.Replace("module Use =\n", $"module Use =\n open {ns}\n\n") + code.Replace("module Use =\n", $"module Use =\n\n open {ns}\n\n") assertFixedCode expected fix.FixedCode Assert.Equal(None, provider |> tryFix fix.FixedCode mode) @@ -213,7 +213,7 @@ let ``Add Open supports a top module header without a blank line`` (atTop: bool) Assert.Equal("open System", fix.Message) - assertFixedCode "module Consumer\nopen System\n\nlet value: DateTime = Unchecked.defaultof<_>\nlet other = 1\n" fix.FixedCode + assertFixedCode "module Consumer\n\nopen System\n\nlet value: DateTime = Unchecked.defaultof<_>\nlet other = 1\n" fix.FixedCode [] [ n | _ -> 0", "Candidates.Qualified.Restricted")>] @@ -370,14 +370,13 @@ let ``completion commit applies case text and avoids duplicate opens`` elif sameFile then let margin = if container = "" then "" else " " let anchor = $"{margin}module Use =\n" - let before = if atTop || container = "" then "\n" else "" - committed.Replace(anchor, $"{before}{margin}open {ns}\n\n{anchor}") + committed.Replace(anchor, $"\n{margin}open {ns}\n\n{anchor}") elif atTop then withOpen true ns committed elif closedInner then - committed.Replace("module Use =\n", $"module Use =\n open {ns}\n\n") + committed.Replace("module Use =\n", $"module Use =\n\n open {ns}\n\n") else - committed.Replace("module Use =\n\n", $"module Use =\n open {ns}\n\n") + withOpen false ns committed assertFixedCode expected actual @@ -551,7 +550,11 @@ let ``referenced case Add Open uses the nearest physical scope without self-qual Assert.Equal("open Outer.Patterns", fix.Message) let actual = text.WithChanges(fix.Changes).ToString() - Assert.Equal(code.Replace(" let classify", " open Outer.Patterns\n\n let classify"), actual.Replace("\r\n", "\n")) + + Assert.Equal( + code.Replace(" let classify", "\n open Outer.Patterns\n\n let classify"), + actual.Replace("\r\n", "\n") + ) let _, results = document.WithText(SourceText.From actual).GetFSharpParseAndCheckResultsAsync("referenced AP edit") diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOffTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOffTests.fs index 765b1aa6210..123962c1942 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOffTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOffTests.fs @@ -6,6 +6,7 @@ open Microsoft.VisualStudio.FSharp.Editor open Xunit open CodeFixTestFramework +open AddOpenOnTopSharedTests let private codeFix = AddOpenCodeFixProvider(AssemblyContentProvider()) @@ -158,6 +159,38 @@ let ``Fixes FS0039 for missing opens - nested module`` () = Assert.Equal(expected, actual) +let private bodyMargin = String.replicate 37 " " +let private declarationMargin = String.replicate 17 " " +let private continuationMargin = String.replicate 21 " " + +let placements = + scenarios + [ + ("nested module declared on one line", + "module Module1 = let x = Console.WriteLine 42\n", + "module Module1 =\n open System\n\n let x = Console.WriteLine 42\n") + + ("use before a nested module declared on one line", + "module Module1\nlet x = Console.WriteLine 42\n[] module Module2 = let y = 1\n", + "module Module1\n\nopen System\n\nlet x = Console.WriteLine 42\n[] module Module2 = let y = 1\n") + + ("use in the attribute of a nested module declared on one line", + "module Module1\n[] module Module2 = let y = 1\n", + "module Module1\n\nopen System\n\n[] module Module2 = let y = 1\n") + + ("use in the body of a nested module declared on one line with an attribute", + "module Module1\n[] module Module2 = let y = Console.WriteLine 42\n", + $"module Module1\n[] module Module2 =\n{bodyMargin}open System\n\n{bodyMargin}let y = Console.WriteLine 42\n") + + ("use after a block comment inside the body of a nested module declared on one line", + $"module Module1\nmodule Module2 = let y = (* body\n{continuationMargin}*) Console.WriteLine 42\n", + $"module Module1\nmodule Module2 =\n{declarationMargin}open System\n\n{declarationMargin}let y = (* body\n{continuationMargin}*) Console.WriteLine 42\n") + ] + +[] +let ``Fixes FS0039 for missing opens - placement`` (_scenario: string, code: string, fixedCode: string) = + expectOpenSystem mode code fixedCode + [] let ``Fixes FS0039 for missing opens - explicit module has attributes`` () = let code = diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOnTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOnTests.fs index 2614a54a493..bc12d8b54f3 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOnTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopOnTests.fs @@ -6,6 +6,7 @@ open Microsoft.VisualStudio.FSharp.Editor open Xunit open CodeFixTestFramework +open AddOpenOnTopSharedTests let private codeFix = AddOpenCodeFixProvider(AssemblyContentProvider()) @@ -78,6 +79,28 @@ Console.WriteLine 42 Assert.Equal(expected, actual) +let placements = + scenarios + [ + ("declaration shares its line with the end of a comment", + "(* header\n*) Console.WriteLine 42\n", + "(* header\n*)\n open System\n\n Console.WriteLine 42\n") + + ("explicit top level module without a blank line", + "module Module1\nConsole.WriteLine 42\n", + "module Module1\n\nopen System\n\nConsole.WriteLine 42\n") + + ("last line with no line break", "module Module1\nConsole.WriteLine 42", "module Module1\n\nopen System\n\nConsole.WriteLine 42") + + ("namespace without a blank line", + "namespace N1\nmodule M1 =\n Console.WriteLine 42\n", + "namespace N1\n\nopen System\n\nmodule M1 =\n Console.WriteLine 42\n") + ] + +[] +let ``Fixes FS0039 for missing opens - placement`` (_scenario: string, code: string, fixedCode: string) = + expectOpenSystem Auto code fixedCode + [] let ``Fixes FS0039 for missing opens - there is already an open directive`` () = let code = diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopSharedTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopSharedTests.fs new file mode 100644 index 00000000000..e107a5b2dd2 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddOpenOnTopSharedTests.fs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.AddOpenOnTopSharedTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = AddOpenCodeFixProvider(AssemblyContentProvider()) + +let scenarios (cases: (string * string * string) list) : obj[] seq = + cases + |> Seq.map (fun (name, code, fixedCode) -> [| box name; box code; box fixedCode |]) + +let expectOpenSystem mode code fixedCode = + let expected = + Some + { + Message = "open System" + FixedCode = fixedCode + } + + Assert.Equal(expected, codeFix |> tryFix code mode) + +let modeOf alwaysPlaceOpensAtTopLevel = + WithSettings + { CodeFixesOptions.Default with + AlwaysPlaceOpensAtTopLevel = alwaysPlaceOpensAtTopLevel + } + +let expectOpenSystemInBothModes code fixedCode = + for alwaysPlaceOpensAtTopLevel in [ true; false ] do + expectOpenSystem (modeOf alwaysPlaceOpensAtTopLevel) code fixedCode + +[] +[] +[] +let ``Fixes FS0039 for missing opens - module has an attribute on the same line`` (alwaysPlaceOpensAtTopLevel: bool) = + expectOpenSystem + (modeOf alwaysPlaceOpensAtTopLevel) + "[] module Module1\n\nConsole.WriteLine 42\n" + "[] module Module1\n\nopen System\n\nConsole.WriteLine 42\n" + +[] +let ``Fixes FS0039 for missing opens - block comment runs on from the module header`` () = + expectOpenSystemInBothModes + "module Module1 (* header\n*)\nConsole.WriteLine 42\n" + "module Module1 (* header\n*)\n\nopen System\n\nConsole.WriteLine 42\n" + +[] +let ``Fixes FS0039 for missing opens - block comment runs on from the namespace header`` () = + expectOpenSystemInBothModes + "namespace Ns (* header\n*)\ntype T() =\n member _.M() = Console.WriteLine 42\n" + "namespace Ns (* header\n*)\n\nopen System\n\ntype T() =\n member _.M() = Console.WriteLine 42\n" + +[] +let ``Fixes FS0039 for missing opens - block comment ending the header shares its line with the first declaration`` () = + expectOpenSystemInBothModes + "module Module1 (* header\n*) let x = Console.WriteLine 42\n" + "module Module1 (* header\n*)\n open System\n\n let x = Console.WriteLine 42\n" diff --git a/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs index 68048314673..81522214d5e 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs @@ -149,6 +149,61 @@ module CompletionProviderTests = Assert.Equal(expected, sourceText.ToString(resultSpan)) + let VerifyCompletionWithOpen (fileContents: string, marker: string, itemName: string, expected: string) = + let caretPosition = fileContents.IndexOf(marker) + marker.Length + let sourceText = SourceText.From(fileContents) + + let document = + RoslynTestHelpers.CreateSolution(fileContents) + |> RoslynTestHelpers.GetSingleDocument + + let span = + CompletionUtils.getDefaultCompletionListSpan ( + sourceText, + caretPosition, + document.Id, + filePath, + [], + None, + CancellationToken.None + ) + + let context = + let provider = + { new CompletionProvider() with + member _.ProvideCompletionsAsync _ = Tasks.Task.CompletedTask + } + + CompletionContext(provider, document, caretPosition, span, CompletionTrigger.Invoke, null, CancellationToken.None) + + let change = + cancellableTask { + let! items = + FSharpCompletionProvider.ProvideCompletionsAsyncAux( + document, + caretPosition, + AssemblyContentProvider().GetAllEntitiesInProjectAndReferencedAssemblies, + false + ) + + let item = + items + |> Seq.find (fun item -> item.DisplayText = itemName && item.Properties.ContainsKey "NamespaceToOpen") + + // CompletionItem.WithSpan is obsolete: the context hands the item its span. + context.AddItem item + + return! + FSharpCompletionProvider.GetChangeAsyncAux( + document, + item, + FSharp.Compiler.EditorServices.OpenStatementInsertionPoint.Nearest + ) + } + |> CancellableTask.start CancellationToken.None + + Assert.Equal(expected, sourceText.WithChanges(change.Result.TextChange).ToString()) + [] let ShouldTriggerCompletionAtCorrectMarkers () = let testCases = @@ -2174,3 +2229,32 @@ let _ = ] |> Map.ofList ) + + [] + let ``Completion that adds an open keeps it out of a nested module declared on one line`` () = + let fileContents = + """module Top +let x = Console.WriteLine 42 +[] module M = let y = 1 +""" + + let expected = + """module Top + +open System + +let x = Console.WriteLine 42 +[] module M = let y = 1 +""" + + VerifyCompletionWithOpen(fileContents, "[] + let ``Completion that adds an open goes into a nested module declared on one line with an attribute for a use in its body`` () = + let fileContents = "module Top\n[] module M = let y = Conso\n" + let margin = String.replicate 24 " " + + let expected = + $"module Top\n[] module M =\n{margin}open System\n\n{margin}let y = Console\n" + + VerifyCompletionWithOpen(fileContents, "let y = Conso", "Console", expected) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index d5d1a09c79a..a687cdcae14 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -66,6 +66,7 @@ +