提交 90474e83 编写于 作者: V Vasily Kirichenko 提交者: Will Smith

Detect AttributeApplication completion context for unfinished attributes (#4126)

* detect AttributeApplication completion context for unfinished attributes

* fix whitespace sensitiveness, add more tests

* more tests

* support ":" in attribute names

fix tests

* fix tests

* pass ParsedInput instead of ParsedInput option to TryGetCompletionContext

* refactor tests
上级 66df7307
......@@ -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<string>()
......@@ -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(@"(?<=\[\<)(?<attribute>(.*?))(?=\>\])", 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 "[<A1; A2; >]" 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 "[<type:AnAttribute>]"
// match the most nested paired [< and >] first
let matches =
insideAttributeApplicationRegex.Matches(lineStr)
|> Seq.cast<Match>
|> 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) =
......
......@@ -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[]
......
......@@ -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
......
#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 [<Literal>] 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 =
[<Test>]
let ``at [<|, applied to nothing``() =
"""
[<(* marker *)
"""
=> Some CompletionContext.AttributeApplication
[<TestCase ("[<(* marker *)", true)>]
[<TestCase ("[<AnAttr(* marker *)", true)>]
[<TestCase ("[<type:(* marker *)", true)>]
[<TestCase ("[<type:AnAttr(* marker *)", true)>]
[<TestCase ("[< (* marker *)", true)>]
[<TestCase ("[<AnAttribute;(* marker *)", true)>]
[<TestCase ("[<AnAttribute; (* marker *)", true)>]
[<TestCase ("[<AnAttribute>][<(* marker *)", true)>]
[<TestCase ("[<AnAttribute>][< (* marker *)", true)>]
[<TestCase ("[<AnAttribute((* marker *)", false)>]
[<TestCase ("[<AnAttribute( (* marker *)", false)>]
[<TestCase ("[<AnAttribute (* marker *)", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute((* marker *)", false)>]
[<TestCase ("[<AnAttribute; AnAttribute((* marker *)", false)>]
let ``incomplete``(lineStr: string, expectAttributeApplicationContext: bool) =
(sprintf """
%s
type T =
{ F: int }
""" lineStr) => (if expectAttributeApplicationContext then Some CompletionContext.AttributeApplication else None)
[<TestCase ("[<(* marker *)>]", true)>]
[<TestCase ("[<AnAttr(* marker *)>]", true)>]
[<TestCase ("[<type:(* marker *)>]", true)>]
[<TestCase ("[<type:AnAttr(* marker *)>]", true)>]
[<TestCase ("[< (* marker *)>]", true)>]
[<TestCase ("[<AnAttribute>][<(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute>][< (* marker *)>]", true)>]
[<TestCase ("[<AnAttribute;(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute; (* marker *) >]", true)>]
[<TestCase ("[<AnAttribute>][<AnAttribute;(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute (* marker *) >]", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute; AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute; AnAttribute( (* marker *)>]", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute; AnAttribute((* marker *)>]", 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
......@@ -194,7 +194,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)
| _ -> ()
......
......@@ -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
[<Attr // expect AttributeUsageAttribute from System namespace
type MyAttr() = inherit Attribute()", true)
[<Attr // expect AttributeUsage from System namespace
type MyAttr() = inherit Attribute()")
[<Test>]
member public this.``Attribute.WhenAttachedToNothing.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
open System
[<Attr // expect AttributeUsageAttribute from System namespace
// nothing here", true)
[<Attr // expect AttributeUsage
// nothing here")
[<Test>]
member public this.``Attribute.WhenAttachedToLetInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper @"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
let f() = 4"
[<Test>]
......@@ -3569,33 +3568,33 @@ let x = query { for bbbb in abbbbc(*D0*) do
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
type MyAttr() = inherit Attribute()", true)
[<Attr // expect AttributeUsage from System namespace
type MyAttr() = inherit Attribute()")
[<Test>]
member public this.``Attribute.WhenAttachedToNothingInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
// nothing here", true)
[<Attr // expect AttributeUsage from System namespace
// nothing here")
[<Test>]
member public this.``Attribute.WhenAttachedToModuleInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
module Foo =
let x = 42", true)
let x = 42")
[<Test>]
member public this.``Attribute.WhenAttachedToModule.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
module Foo =
let x = 42", true)
let x = 42")
[<Test>]
member public this.``Identifer.InMatchStatemente.Bug72595``() =
......@@ -5052,7 +5051,7 @@ let x = query { for bbbb in abbbbc(*D0*) do
[<
"""]
"[<"
["AttributeUsageAttribute"]
["AttributeUsage"]
[]
[<Test>]
......@@ -5063,7 +5062,7 @@ let x = query { for bbbb in abbbbc(*D0*) do
[<
"""]
"[<"
["AttributeUsageAttribute"]
["AttributeUsage"]
[]
[<Test>]
......
......@@ -97,6 +97,9 @@
<Compile Include="..\..\..\tests\service\AssemblyContentProviderTests.fs">
<Link>AssemblyContentProviderTests.fs</Link>
</Compile>
<Compile Include="..\..\..\tests\service\ServiceUntypedParseTests.fs">
<Link>ServiceUntypedParseTests.fs</Link>
</Compile>
<Compile Include="UnusedOpensTests.fs">
<Link>ServiceAnalysis\UnusedOpensTests.fs</Link>
</Compile>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册