33namespace Microsoft.VisualStudio.FSharp.Editor
44
55open System
6+ open System.Text .RegularExpressions
7+ open System.IO
68open System.Collections .Immutable
79open System.Threading
810open System.Threading .Tasks
911open Microsoft.CodeAnalysis
1012open Microsoft.CodeAnalysis .Completion
1113open Microsoft.CodeAnalysis .Editor .Implementation .IntelliSense .Completion .FileSystem
1214open Microsoft.CodeAnalysis .Text
15+ open Microsoft.CodeAnalysis .Classification
16+
17+ type internal Completion =
18+ { DirectiveRegex: Regex
19+ AllowableExtensions: string list
20+ UseIncludeDirectives: bool }
21+ static member Create ( directiveRegex , allowableExtensions , useIncludeDirectives ) =
22+ { DirectiveRegex= Regex( directiveRegex, RegexOptions.Compiled||| RegexOptions.ExplicitCapture)
23+ AllowableExtensions= allowableExtensions
24+ UseIncludeDirectives= useIncludeDirectives}
25+
26+ type internal HashDirectiveCompletionProvider ( workspace : Workspace , projectInfoManager : ProjectInfoManager , completions : Completion list ) =
27+ inherit CommonCompletionProvider()
1328
14- open System.Text .RegularExpressions
15- open System.IO
16-
17- module internal FileSystemCompletion =
18- let [<Literal>] private NetworkPath = " \\\\ "
19- let private commitRules = ImmutableArray.Create( CharacterSetModificationRule.Create( CharacterSetModificationKind.Replace, '"' , '\\' , ',' , '/' ))
20- let private rules = CompletionItemRules.Create( commitCharacterRules= commitRules)
29+ let [<Literal>] NetworkPath = " \\\\ "
30+ let commitRules = ImmutableArray.Create( CharacterSetModificationRule.Create( CharacterSetModificationKind.Replace, '"' , '\\' , ',' , '/' ))
31+ let rules = CompletionItemRules.Create( commitCharacterRules= commitRules)
2132
22- let private getQuotedPathStart ( text : SourceText , position : int , quotedPathGroup : Group ) =
33+ let getQuotedPathStart ( text : SourceText , position : int , quotedPathGroup : Group ) =
2334 text.Lines.GetLineFromPosition( position) .Start+ quotedPathGroup.Index
2435
25- let private getPathThroughLastSlash ( text : SourceText , position : int , quotedPathGroup : Group ) =
36+ let getPathThroughLastSlash ( text : SourceText , position : int , quotedPathGroup : Group ) =
2637 PathCompletionUtilities.GetPathThroughLastSlash(
2738 quotedPath= quotedPathGroup.Value,
2839 quotedPathStart= getQuotedPathStart( text, position, quotedPathGroup),
2940 position= position)
3041
31- let private getTextChangeSpan ( text : SourceText , position : int , quotedPathGroup : Group ) =
42+ let getTextChangeSpan ( text : SourceText , position : int , quotedPathGroup : Group ) =
3243 PathCompletionUtilities.GetTextChangeSpan(
3344 quotedPath= quotedPathGroup.Value,
3445 quotedPathStart= getQuotedPathStart( text, position, quotedPathGroup),
3546 position= position)
3647
37- let private getFileGlyph ( extention : string ) =
48+ let getFileGlyph ( extention : string ) =
3849match extentionwith
3950| " .exe" | " .dll" -> Some Glyph.Assembly
4051| _ -> None
4152
42- let getItems ( provider : CompletionProvider , document : Document , position : int , allowableExtensions : string list , directiveRegex : Regex , searchPaths : string list ) =
43- asyncMaybe {
44- do ! Option.guard( Path.GetExtension document.FilePath= " .fsx" )
53+ let includeDirectiveCleanRegex = Regex( """ #I\s+(@?"*(?<literal>[^"]*)"?)""" , RegexOptions.Compiled||| RegexOptions.ExplicitCapture)
54+
55+ let getColorizationData ( text : SourceText , position : int ) : ResizeArray < ClassifiedSpan > =
56+ let documentId = workspace.GetDocumentIdInCurrentContext( text.Container)
57+ let document = workspace.CurrentSolution.GetDocument( documentId)
58+ let defines = projectInfoManager.GetCompilationDefinesForEditingDocument( document)
59+ let textLines = text.Lines
60+ let triggerLine = textLines.GetLineFromPosition( position)
61+ CommonHelpers.getColorizationData( documentId, text, triggerLine.Span, Some document.FilePath, defines, CancellationToken.None)
62+
63+ let isInStringLiteral ( text : SourceText , position : int ) : bool =
64+ getColorizationData( text, position)
65+ |> Seq.exists( fun classifiedSpan ->
66+ classifiedSpan.TextSpan.IntersectsWith position&&
67+ classifiedSpan.ClassificationType= ClassificationTypeNames.StringLiteral)
68+
69+ let getIncludeDirectives ( text : SourceText , position : int ) =
70+ let lines = text.Lines
71+ let caretLine = text.Lines.GetLinePosition( position) .Line
72+ lines
73+ |> Seq.filter( fun x -> x.LineNumber< caretLine)
74+ |> Seq.choose( fun line ->
75+ let lineStr = line.ToString() .Trim()
76+ // optimization: fail fast if the line does not start with "(optional spaces) #I"
77+ if not ( lineStr.StartsWith" #I" ) then None
78+ else
79+ match includeDirectiveCleanRegex.Match lineStrwith
80+ | mwhen m.Success->
81+ getColorizationData( text, line.Start)
82+ |> Seq.tryPick( fun span ->
83+ if span.TextSpan.IntersectsWith line.Start&&
84+ ( span.ClassificationType<> ClassificationTypeNames.Comment&&
85+ span.ClassificationType<> ClassificationTypeNames.ExcludedCode) then
86+ Some( m.Groups.[ " literal" ]. Value)
87+ else None)
88+ | _ -> None
89+ )
90+ |> Seq.toList
91+
92+ override this.ProvideCompletionsAsync ( context ) =
93+ asyncMaybe {
94+ let document = context.Document
95+ let position = context.Position
96+ do ! let extension = Path.GetExtension document.FilePath
97+ Option.guard( extension= " .fsx" || extension= " .fsscript" )
98+
4599let! ct = liftAsync Async.CancellationToken
46- let! text = document.GetTextAsync ct
100+ let! text = document.GetTextAsync( ct)
101+ do ! Option.guard( isInStringLiteral( text, position))
47102let line = text.Lines.GetLineFromPosition( position)
48- let lineText = text.ToString( TextSpan.FromBounds( line.Start, position));
49- let m = directiveRegex.Match lineText
50-
51- do ! Option.guard m.Success
52- let quotedPathGroup = m.Groups.[ " literal" ]
53- let quotedPath = quotedPathGroup.Value;
54- let endsWithQuote = PathCompletionUtilities.EndsWithQuote( quotedPath)
103+ let lineText = text.ToString( TextSpan.FromBounds( line.Start, position))
55104
56- do ! Option.guard( not ( endsWithQuote&& ( position>= line.Start+ m.Length)))
105+ let! completion , quotedPathGroup =
106+ completions|> List.tryPick( fun completion ->
107+ match completion.DirectiveRegex.Match lineTextwith
108+ | mwhen m.Success->
109+ let quotedPathGroup = m.Groups.[ " literal" ]
110+ let endsWithQuote = PathCompletionUtilities.EndsWithQuote( quotedPathGroup.Value)
111+ if endsWithQuote&& ( position>= line.Start+ m.Length) then
112+ None
113+ else
114+ Some( completion, quotedPathGroup)
115+ | _ -> None)
116+
57117let snapshot = text.FindCorrespondingEditorTextSnapshot()
58118
59119do ! Option.guard( not ( isNull snapshot))
60120let fileSystem = CurrentWorkingDirectoryDiscoveryService.GetService( snapshot)
61121
122+ let extraSearchPaths =
123+ if completion.UseIncludeDirectivesthen
124+ getIncludeDirectives( text, position)
125+ else []
126+
127+ let defaultSearchPath = Path.GetDirectoryName document.FilePath
128+ let searchPaths = defaultSearchPath:: extraSearchPaths
129+
62130let helper =
63131 FileSystemCompletionHelper(
64- provider ,
132+ this ,
65133 getTextChangeSpan( text, position, quotedPathGroup),
66134 fileSystem,
67135 Glyph.OpenFolder,
68- allowableExtensions |> List.tryPick getFileGlyph|> Option.defaultValue Glyph.None,
136+ completion.AllowableExtensions |> List.tryPick getFileGlyph|> Option.defaultValue Glyph.None,
69137 searchPaths= Seq.toImmutableArray searchPaths,
70- allowableExtensions= allowableExtensions ,
138+ allowableExtensions= completion.AllowableExtensions ,
71139 itemRules= rules)
72140
73141let pathThroughLastSlash = getPathThroughLastSlash( text, position, quotedPathGroup)
74142let documentPath = if document.Project.IsSubmissionthen null else document.FilePath
75- return helper.GetItems( pathThroughLastSlash, documentPath)
76- } |> Async.map( Option.defaultValue ImmutableArray.Empty)
77-
78- let isInsertionTrigger ( text : SourceText , position ) =
143+ context.AddItems( helper.GetItems( pathThroughLastSlash, documentPath))
144+ }
145+ |> Async.Ignore
146+ |> CommonRoslynHelpers.StartAsyncUnitAsTask context.CancellationToken
147+
148+ override __.IsInsertionTrigger ( text , position , _ ) =
79149// Bring up completion when the user types a quote (i.e.: #r "), or if they type a slash
80150// path separator character, or if they type a comma (#r "foo,version...").
81151// Also, if they're starting a word. i.e. #r "c:\W
82152let ch = text.[ position]
83- ch= '"' || ch= '\\' || ch= ',' || ch= '/' ||
84- CommonCompletionUtilities.IsStartingNewWord( text, position, ( fun x -> Char.IsLetter x), ( fun x -> Char.IsLetterOrDigit x))
153+ let isTriggerChar =
154+ ch= '"' || ch= '\\' || ch= ',' || ch= '/' ||
155+ CommonCompletionUtilities.IsStartingNewWord( text, position, ( fun x -> Char.IsLetter x), ( fun x -> Char.IsLetterOrDigit x))
156+ isTriggerChar&& isInStringLiteral( text, position)
85157
86- let getTextChange ( selectedItem : CompletionItem , ch : Nullable < char > )=
158+ override __.GetTextChangeAsync ( selectedItem , ch , cancellationToken ) =
87159// When we commit "\\" when the user types \ we have to adjust for the fact that the
88160// controller will automatically append \ after we commit. Because of that, we don't
89161// want to actually commit "\\" as we'll end up with "\\\". So instead we just commit
90162// "\" and know that controller will append "\" and give us "\\".
91163if selectedItem.DisplayText= NetworkPath&& ch= Nullable'\\' then
92- Some ( TextChange( selectedItem.Span, " \\ " ))
164+ Task.FromResult ( Nullable ( TextChange( selectedItem.Span, " \\ " ) ))
93165else
94- None
95-
96- let private includeDirectiveCleanRegex = Regex( """ #I\s+(@?"*(?<literal>[^"]*)"?)""" , RegexOptions.Compiled||| RegexOptions.ExplicitCapture)
97-
98- let getIncludeDirectives ( document : Document , position : int ) =
99- async {
100- let! ct = Async.CancellationToken
101- let! text = document.GetTextAsync( ct)
102- let lines = text.Lines
103- let caretLine = text.Lines.GetLinePosition( position). Line
104- return
105- lines
106- |> Seq.filter( fun x -> x.LineNumber<= caretLine)
107- |> Seq.choose( fun line ->
108- let lineStr = line.ToString() .Trim()
109- // optimization: fail fast if the line does not start with "(optional spaces) #I"
110- if not ( lineStr.StartsWith" #I" ) then None
111- else
112- match includeDirectiveCleanRegex.Match lineStrwith
113- | mwhen m.Success-> Some( m.Groups.[ " literal" ]. Value)
114- | _ -> None
115- )
116- |> Seq.toList
117- }
118-
119- [<AbstractClass>]
120- type internal HashDirectiveCompletionProvider ( directiveRegex : string , allowableExtensions : string list , useIncludeDirectives : bool ) =
121- inherit CommonCompletionProvider()
122-
123- let directiveRegex = Regex( directiveRegex, RegexOptions.Compiled||| RegexOptions.ExplicitCapture)
124-
125- override this.ProvideCompletionsAsync ( context ) =
126- async {
127- let defaultSearchPath = Path.GetDirectoryName context.Document.FilePath
128- let! extraSearchPaths =
129- if useIncludeDirectivesthen
130- FileSystemCompletion.getIncludeDirectives( context.Document, context.Position)
131- else async.Return[]
132- let searchPaths = defaultSearchPath:: extraSearchPaths
133- let! items = FileSystemCompletion.getItems( this, context.Document, context.Position, allowableExtensions, directiveRegex, searchPaths)
134- context.AddItems( items)
135- } |> CommonRoslynHelpers.StartAsyncUnitAsTask context.CancellationToken
136-
137- override __.IsInsertionTrigger ( text , position , _ ) = FileSystemCompletion.isInsertionTrigger( text, position)
138-
139- override __.GetTextChangeAsync ( selectedItem , ch , cancellationToken ) =
140- match FileSystemCompletion.getTextChange( selectedItem, ch) with
141- | Some x-> Task.FromResult( Nullable x)
142- | None-> base .GetTextChangeAsync( selectedItem, ch, cancellationToken)
143-
144-
145- type internal LoadDirectiveCompletionProvider () =
146- inherit HashDirectiveCompletionProvider( """ \s*#load\s+(@?"*(?<literal>"[^"]*"?))""" , [ " .fs" ; " .fsx" ], useIncludeDirectives= true )
147-
148- type internal ReferenceDirectiveCompletionProvider () =
149- inherit HashDirectiveCompletionProvider( """ \s*#r\s+(@?"*(?<literal>"[^"]*"?))""" , [ " .dll" ; " .exe" ], useIncludeDirectives= true )
150-
151- type internal IncludeDirectiveCompletionProvider () =
152- // we have to pass an extension that's not met in real life because if we pass empty list, it does not filter at all.
153- inherit HashDirectiveCompletionProvider( """ \s*#I\s+(@?"*(?<literal>"[^"]*"?))""" , [ " .impossible_extension" ], useIncludeDirectives= false )
166+ base .GetTextChangeAsync( selectedItem, ch, cancellationToken)