diff --git a/src/fsharp/service/ServiceUntypedParse.fs b/src/fsharp/service/ServiceUntypedParse.fs index 1eb1bf5054a..2b79da350af 100755 --- a/src/fsharp/service/ServiceUntypedParse.fs +++ b/src/fsharp/service/ServiceUntypedParse.fs @@ -411,6 +411,8 @@ type EntityKind = override x.ToString() = sprintf "%A" x module UntypedParseImpl = + open System.Text.RegularExpressions + open Microsoft.FSharp.Compiler.PrettyNaming let emptyStringSet = HashSet() @@ -932,20 +934,13 @@ module UntypedParseImpl = | ParsedInput.ImplFile input -> walkImplFileInput input type internal TS = AstTraversal.TraverseStep + /// Matches the most nested [< and >] pair. + let insideAttributeApplicationRegex = Regex(@"(?<=\[\<)(?(.*?))(?=\>\])", RegexOptions.Compiled ||| RegexOptions.ExplicitCapture) /// Try to determine completion context for the given pair (row, columns) - let TryGetCompletionContext (pos, untypedParseOpt: FSharpParseFileResults option, lineStr: string) : CompletionContext option = - let parsedInputOpt = - match untypedParseOpt with - | Some upi -> upi.ParseTree - | None -> None - - match parsedInputOpt with - | None -> None - | Some pt -> + let TryGetCompletionContext (pos, parsedInput: ParsedInput, lineStr: string) : CompletionContext option = - - match GetEntityKind(pos, pt) with + match GetEntityKind(pos, parsedInput) with | Some EntityKind.Attribute -> Some CompletionContext.AttributeApplication | _ -> @@ -1282,7 +1277,48 @@ module UntypedParseImpl = | _ -> defaultTraverse ty } - AstTraversal.Traverse(pos, pt, walker) + AstTraversal.Traverse(pos, parsedInput, walker) + // Uncompleted attribute applications are not presented in the AST in any way. So, we have to parse source string. + |> Option.orElseWith (fun _ -> + let cutLeadingAttributes (str: string) = + // cut off leading attributes, i.e. we cut "[]" to " >]" + match str.LastIndexOf ';' with + | -1 -> str + | idx when idx < str.Length -> str.[idx + 1..].TrimStart() + | _ -> "" + + let isLongIdent = Seq.forall (fun c -> IsIdentifierPartCharacter c || c = '.' || c = ':') // ':' may occur in "[]" + + // match the most nested paired [< and >] first + let matches = + insideAttributeApplicationRegex.Matches(lineStr) + |> Seq.cast + |> Seq.filter (fun m -> m.Index <= pos.Column && m.Index + m.Length >= pos.Column) + |> Seq.toArray + + if not (Array.isEmpty matches) then + matches + |> Seq.tryPick (fun m -> + let g = m.Groups.["attribute"] + let col = pos.Column - g.Index + if col >= 0 && col < g.Length then + let str = g.Value.Substring(0, col).TrimStart() // cut other rhs attributes + let str = cutLeadingAttributes str + if isLongIdent str then + Some CompletionContext.AttributeApplication + else None + else None) + else + // Paired [< and >] were not found, try to determine that we are after [< without closing >] + match lineStr.LastIndexOf "[<" with + | -1 -> None + | openParenIndex when pos.Column >= openParenIndex + 2 -> + let str = lineStr.[openParenIndex + 2..pos.Column - 1].TrimStart() + let str = cutLeadingAttributes str + if isLongIdent str then + Some CompletionContext.AttributeApplication + else None + | _ -> None) /// Check if we are at an "open" declaration let GetFullNameOfSmallestModuleOrNamespaceAtPoint (parsedInput: ParsedInput, pos: pos) = diff --git a/src/fsharp/service/ServiceUntypedParse.fsi b/src/fsharp/service/ServiceUntypedParse.fsi index fc22ca5d223..2caf81f6fb0 100755 --- a/src/fsharp/service/ServiceUntypedParse.fsi +++ b/src/fsharp/service/ServiceUntypedParse.fsi @@ -105,7 +105,7 @@ module public UntypedParseImpl = val TryFindExpressionASTLeftOfDotLeftOfCursor : pos * ParsedInput option -> (pos * bool) option val GetRangeOfExprLeftOfDot : pos * ParsedInput option -> range option val TryFindExpressionIslandInPosition : pos * ParsedInput option -> string option - val TryGetCompletionContext : pos * FSharpParseFileResults option * lineStr: string -> CompletionContext option + val TryGetCompletionContext : pos * ParsedInput * lineStr: string -> CompletionContext option val GetEntityKind: pos * ParsedInput -> EntityKind option val GetFullNameOfSmallestModuleOrNamespaceAtPoint : ParsedInput * pos -> string[] diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index f40cc61dc46..80b55834f02 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -780,7 +780,11 @@ type TypeCheckInfo | otherwise -> otherwise - 1 // Look for a "special" completion context - let completionContext = UntypedParseImpl.TryGetCompletionContext(mkPos line colAtEndOfNamesAndResidue, parseResultsOpt, lineStr) + let completionContext = + parseResultsOpt + |> Option.bind (fun x -> x.ParseTree) + |> Option.bind (fun parseTree -> UntypedParseImpl.TryGetCompletionContext(mkPos line colAtEndOfNamesAndResidue, parseTree, lineStr)) + let res = match completionContext with // Invalid completion locations diff --git a/tests/service/ServiceUntypedParseTests.fs b/tests/service/ServiceUntypedParseTests.fs new file mode 100644 index 00000000000..0ad29b23e4d --- /dev/null +++ b/tests/service/ServiceUntypedParseTests.fs @@ -0,0 +1,102 @@ +#if INTERACTIVE +#r "../../Debug/fcs/net45/FSharp.Compiler.Service.dll" // note, run 'build fcs debug' to generate this, this DLL has a public API so can be used from F# Interactive +#r "../../packages/NUnit.3.5.0/lib/net45/nunit.framework.dll" +#load "FsUnit.fs" +#load "Common.fs" +#else +module Tests.Service.ServiceUntypedParseTests +#endif + +open System +open System.IO +open System.Text +open NUnit.Framework +open Microsoft.FSharp.Compiler.Range +open Microsoft.FSharp.Compiler.SourceCodeServices +open FSharp.Compiler.Service.Tests.Common +open Tests.Service + +let [] private Marker = "(* marker *)" + +let private (=>) (source: string) (expected: CompletionContext option) = + + let lines = + use reader = new StringReader(source) + [| let line = ref (reader.ReadLine()) + while not (isNull !line) do + yield !line + line := reader.ReadLine() + if source.EndsWith "\n" then + yield "" |] + + let markerPos = + lines + |> Array.mapi (fun i x -> i, x) + |> Array.tryPick (fun (lineIdx, line) -> + match line.IndexOf Marker with + | -1 -> None + | idx -> Some (mkPos (Line.fromZ lineIdx) idx)) + + match markerPos with + | None -> failwithf "Marker '%s' was not found in the source code" Marker + | Some markerPos -> + match parseSourceCode("C:\\test.fs", source) with + | None -> failwith "No parse tree" + | Some parseTree -> + let actual = UntypedParseImpl.TryGetCompletionContext(markerPos, parseTree, lines.[Line.toZ markerPos.Line]) + try Assert.AreEqual(expected, actual) + with e -> + printfn "ParseTree: %A" parseTree + reraise() + +module AttributeCompletion = + [] + let ``at [<|, applied to nothing``() = + """ +[<(* marker *) +""" + => Some CompletionContext.AttributeApplication + + [] + [] + [] + [] + [] + [] + [] + [][<(* marker *)", true)>] + [][< (* marker *)", true)>] + [] + [] + [] + [][] + [] + let ``incomplete``(lineStr: string, expectAttributeApplicationContext: bool) = + (sprintf """ +%s +type T = + { F: int } +""" lineStr) => (if expectAttributeApplicationContext then Some CompletionContext.AttributeApplication else None) + + []", true)>] + []", true)>] + []", true)>] + []", true)>] + []", true)>] + [][<(* marker *)>]", true)>] + [][< (* marker *)>]", true)>] + []", true)>] + []", true)>] + [][]", true)>] + []", false)>] + []", false)>] + [][]", false)>] + []", false)>] + []", false)>] + [][]", false)>] + let ``complete``(lineStr: string, expectAttributeApplicationContext: bool) = + (sprintf """ +%s +type T = + { F: int } +""" lineStr) => (if expectAttributeApplicationContext then Some CompletionContext.AttributeApplication else None) \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs index a7007136f98..c995095bdaf 100644 --- a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs @@ -198,7 +198,13 @@ type internal FSharpCompletionProvider if results.Count > 0 && not declarations.IsForType && not declarations.IsError && List.isEmpty partialName.QualifyingIdents then let lineStr = textLines.[caretLinePos.Line].ToString() - match UntypedParseImpl.TryGetCompletionContext(Pos.fromZ caretLinePos.Line caretLinePos.Character, Some parseResults, lineStr) with + + let completionContext = + parseResults.ParseTree + |> Option.bind (fun parseTree -> + UntypedParseImpl.TryGetCompletionContext(Pos.fromZ caretLinePos.Line caretLinePos.Character, parseTree, lineStr)) + + match completionContext with | None -> results.AddRange(keywordCompletionItems) | _ -> () diff --git a/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs b/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs index df4776846c8..8135302bcb1 100644 --- a/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs +++ b/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs @@ -182,9 +182,8 @@ type UsingMSBuild() as this = shouldContain // should contain shouldNotContain - member public this.AutoCompleteBug70080Helper(programText:string, ?withSuffix: bool) = - let expected = if defaultArg withSuffix false then "AttributeUsageAttribute" else "AttributeUsage" - this.AutoCompleteBug70080HelperHelper(programText, [expected], []) + member public this.AutoCompleteBug70080Helper(programText: string) = + this.AutoCompleteBug70080HelperHelper(programText, ["AttributeUsage"], []) member private this.testAutoCompleteAdjacentToDot op = let text = sprintf "System.Console%s" op @@ -3546,22 +3545,22 @@ let x = query { for bbbb in abbbbc(*D0*) do member public this.``Attribute.WhenAttachedToType.Bug70080``() = this.AutoCompleteBug70080Helper(@" open System - [] member public this.``Attribute.WhenAttachedToNothing.Bug70080``() = this.AutoCompleteBug70080Helper(@" open System - [] member public this.``Attribute.WhenAttachedToLetInNamespace.Bug70080``() = this.AutoCompleteBug70080Helper @" namespace Foo open System - [] @@ -3569,33 +3568,33 @@ let x = query { for bbbb in abbbbc(*D0*) do this.AutoCompleteBug70080Helper(@" namespace Foo open System - [] member public this.``Attribute.WhenAttachedToNothingInNamespace.Bug70080``() = this.AutoCompleteBug70080Helper(@" namespace Foo open System - [] member public this.``Attribute.WhenAttachedToModuleInNamespace.Bug70080``() = this.AutoCompleteBug70080Helper(@" namespace Foo open System - [] member public this.``Attribute.WhenAttachedToModule.Bug70080``() = this.AutoCompleteBug70080Helper(@" open System - [] member public this.``Identifer.InMatchStatemente.Bug72595``() = @@ -5052,7 +5051,7 @@ let x = query { for bbbb in abbbbc(*D0*) do [< """] "[<" - ["AttributeUsageAttribute"] + ["AttributeUsage"] [] [] @@ -5063,7 +5062,7 @@ let x = query { for bbbb in abbbbc(*D0*) do [< """] "[<" - ["AttributeUsageAttribute"] + ["AttributeUsage"] [] [] diff --git a/vsintegration/tests/unittests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/unittests/VisualFSharp.UnitTests.fsproj index 23fc9e72fb4..3734a99d108 100644 --- a/vsintegration/tests/unittests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/unittests/VisualFSharp.UnitTests.fsproj @@ -97,6 +97,9 @@ AssemblyContentProviderTests.fs + + ServiceUntypedParseTests.fs + ServiceAnalysis\UnusedOpensTests.fs