Disable completion inside comments and excluded code (#2505)

* disable completion inside comments and excluded code

refactoring

* disable normal completion inside comments and excluded code

* better include directive selection
This commit is contained in:
Vasily Kirichenko 2017-02-28 21:24:38 +03:00 коммит произвёл Kevin Ransom (msft)
Родитель df1276e1e4
Коммит 50838327a4
3 изменённых файлов: 134 добавлений и 120 удалений

Просмотреть файл

@ -38,16 +38,28 @@ type internal FSharpCompletionProvider
let xmlMemberIndexService = serviceProvider.GetService(typeof<IVsXMLMemberIndexService>) :?> IVsXMLMemberIndexService
let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService, serviceProvider.DTE)
static let attributeSuffixLength = "Attribute".Length
static let shouldProvideCompletion (documentId: DocumentId, filePath: string, defines: string list, text: SourceText, position: int) : bool =
let textLines = text.Lines
let triggerLine = textLines.GetLineFromPosition position
let colorizationData = CommonHelpers.getColorizationData(documentId, text, triggerLine.Span, Some filePath, defines, CancellationToken.None)
colorizationData.Count = 0 || // we should provide completion at the start of empty line, where there are no tokens at all
colorizationData.Exists (fun classifiedSpan ->
classifiedSpan.TextSpan.IntersectsWith position &&
(
match classifiedSpan.ClassificationType with
| ClassificationTypeNames.Comment
| ClassificationTypeNames.StringLiteral
| ClassificationTypeNames.ExcludedCode
| ClassificationTypeNames.NumericLiteral -> false
| _ -> true // anything else is a valid classification type
))
static member ShouldTriggerCompletionAux(sourceText: SourceText, caretPosition: int, trigger: CompletionTriggerKind, getInfo: (unit -> DocumentId * string * string list)) =
// Skip if we are at the start of a document
if caretPosition = 0 then
false
if caretPosition = 0 then false
// Skip if it was triggered by an operation other than insertion
elif not (trigger = CompletionTriggerKind.Insertion) then
false
elif not (trigger = CompletionTriggerKind.Insertion) then false
// Skip if we are not on a completion trigger
else
let triggerPosition = caretPosition - 1
@ -63,22 +75,7 @@ type internal FSharpCompletionProvider
// Trigger completion if we are on a valid classification type
else
let documentId, filePath, defines = getInfo()
let textLines = sourceText.Lines
let triggerLine = textLines.GetLineFromPosition(triggerPosition)
let classifiedSpanOption =
CommonHelpers.getColorizationData(documentId, sourceText, triggerLine.Span, Some(filePath), defines, CancellationToken.None)
|> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(triggerPosition))
match classifiedSpanOption with
| None -> false
| Some(classifiedSpan) ->
match classifiedSpan.ClassificationType with
| ClassificationTypeNames.Comment
| ClassificationTypeNames.StringLiteral
| ClassificationTypeNames.ExcludedCode
| ClassificationTypeNames.NumericLiteral -> false
| _ -> true // anything else is a valid classification type
shouldProvideCompletion(documentId, filePath, defines, sourceText, triggerPosition)
static member ProvideCompletionsAsyncAux(checker: FSharpChecker, sourceText: SourceText, caretPosition: int, options: FSharpProjectOptions, filePath: string, textVersionHash: int) =
asyncMaybe {
@ -131,10 +128,13 @@ type internal FSharpCompletionProvider
override this.ProvideCompletionsAsync(context: Microsoft.CodeAnalysis.Completion.CompletionContext) =
asyncMaybe {
let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(context.Document)
let document = context.Document
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document)
do! Option.guard (shouldProvideCompletion(document.Id, document.FilePath, defines, sourceText, context.Position))
let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document)
let! textVersion = context.Document.GetTextVersionAsync(context.CancellationToken)
let! results = FSharpCompletionProvider.ProvideCompletionsAsyncAux(checkerProvider.Checker, sourceText, context.Position, options, context.Document.FilePath, textVersion.GetHashCode())
let! results = FSharpCompletionProvider.ProvideCompletionsAsyncAux(checkerProvider.Checker, sourceText, context.Position, options, document.FilePath, textVersion.GetHashCode())
context.AddItems(results)
} |> Async.Ignore |> CommonRoslynHelpers.StartAsyncUnitAsTask context.CancellationToken

Просмотреть файл

@ -24,9 +24,10 @@ type internal FSharpCompletionService
let builtInProviders =
ImmutableArray.Create<CompletionProvider>(
FSharpCompletionProvider(workspace, serviceProvider, checkerProvider, projectInfoManager),
ReferenceDirectiveCompletionProvider(),
LoadDirectiveCompletionProvider(),
IncludeDirectiveCompletionProvider()
HashDirectiveCompletionProvider(workspace, projectInfoManager,
[ Completion.Create("""\s*#load\s+(@?"*(?<literal>"[^"]*"?))""", [".fs"; ".fsx"], useIncludeDirectives = true)
Completion.Create("""\s*#r\s+(@?"*(?<literal>"[^"]*"?))""", [".dll"; ".exe"], useIncludeDirectives = true)
Completion.Create("""\s*#I\s+(@?"*(?<literal>"[^"]*"?))""", ["\x00"], useIncludeDirectives = false) ])
// we've turned off keyword completion because it does not filter suggestion depending on context.
// FSharpKeywordCompletionProvider(workspace, projectInfoManager)
)

Просмотреть файл

@ -3,6 +3,8 @@
namespace Microsoft.VisualStudio.FSharp.Editor
open System
open System.Text.RegularExpressions
open System.IO
open System.Collections.Immutable
open System.Threading
open System.Threading.Tasks
@ -10,144 +12,155 @@ open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Completion
open Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion.FileSystem
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Classification
open System.Text.RegularExpressions
open System.IO
type internal Completion =
{ DirectiveRegex: Regex
AllowableExtensions: string list
UseIncludeDirectives: bool }
static member Create(directiveRegex, allowableExtensions, useIncludeDirectives) =
{ DirectiveRegex = Regex(directiveRegex, RegexOptions.Compiled ||| RegexOptions.ExplicitCapture)
AllowableExtensions = allowableExtensions
UseIncludeDirectives = useIncludeDirectives }
module internal FileSystemCompletion =
let [<Literal>] private NetworkPath = "\\\\"
let private commitRules = ImmutableArray.Create(CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '"', '\\', ',', '/'))
let private rules = CompletionItemRules.Create(commitCharacterRules = commitRules)
type internal HashDirectiveCompletionProvider(workspace: Workspace, projectInfoManager: ProjectInfoManager, completions: Completion list) =
inherit CommonCompletionProvider()
let private getQuotedPathStart(text: SourceText, position: int, quotedPathGroup: Group) =
let [<Literal>] NetworkPath = "\\\\"
let commitRules = ImmutableArray.Create(CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '"', '\\', ',', '/'))
let rules = CompletionItemRules.Create(commitCharacterRules = commitRules)
let getQuotedPathStart(text: SourceText, position: int, quotedPathGroup: Group) =
text.Lines.GetLineFromPosition(position).Start + quotedPathGroup.Index
let private getPathThroughLastSlash(text: SourceText, position: int, quotedPathGroup: Group) =
let getPathThroughLastSlash(text: SourceText, position: int, quotedPathGroup: Group) =
PathCompletionUtilities.GetPathThroughLastSlash(
quotedPath = quotedPathGroup.Value,
quotedPathStart = getQuotedPathStart(text, position, quotedPathGroup),
position = position)
let private getTextChangeSpan(text: SourceText, position: int, quotedPathGroup: Group) =
let getTextChangeSpan(text: SourceText, position: int, quotedPathGroup: Group) =
PathCompletionUtilities.GetTextChangeSpan(
quotedPath = quotedPathGroup.Value,
quotedPathStart = getQuotedPathStart(text, position, quotedPathGroup),
position = position)
let private getFileGlyph (extention: string) =
let getFileGlyph (extention: string) =
match extention with
| ".exe" | ".dll" -> Some Glyph.Assembly
| _ -> None
let getItems(provider: CompletionProvider, document: Document, position: int, allowableExtensions: string list, directiveRegex: Regex, searchPaths: string list) =
asyncMaybe {
do! Option.guard (Path.GetExtension document.FilePath = ".fsx")
let includeDirectiveCleanRegex = Regex("""#I\s+(@?"*(?<literal>[^"]*)"?)""", RegexOptions.Compiled ||| RegexOptions.ExplicitCapture)
let getColorizationData(text: SourceText, position: int) : ResizeArray<ClassifiedSpan> =
let documentId = workspace.GetDocumentIdInCurrentContext(text.Container)
let document = workspace.CurrentSolution.GetDocument(documentId)
let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document)
let textLines = text.Lines
let triggerLine = textLines.GetLineFromPosition(position)
CommonHelpers.getColorizationData(documentId, text, triggerLine.Span, Some document.FilePath, defines, CancellationToken.None)
let isInStringLiteral(text: SourceText, position: int) : bool =
getColorizationData(text, position)
|> Seq.exists(fun classifiedSpan ->
classifiedSpan.TextSpan.IntersectsWith position &&
classifiedSpan.ClassificationType = ClassificationTypeNames.StringLiteral)
let getIncludeDirectives (text: SourceText, position: int) =
let lines = text.Lines
let caretLine = text.Lines.GetLinePosition(position).Line
lines
|> Seq.filter (fun x -> x.LineNumber < caretLine)
|> Seq.choose (fun line ->
let lineStr = line.ToString().Trim()
// optimization: fail fast if the line does not start with "(optional spaces) #I"
if not (lineStr.StartsWith "#I") then None
else
match includeDirectiveCleanRegex.Match lineStr with
| m when m.Success ->
getColorizationData(text, line.Start)
|> Seq.tryPick (fun span ->
if span.TextSpan.IntersectsWith line.Start &&
(span.ClassificationType <> ClassificationTypeNames.Comment &&
span.ClassificationType <> ClassificationTypeNames.ExcludedCode) then
Some (m.Groups.["literal"].Value)
else None)
| _ -> None
)
|> Seq.toList
override this.ProvideCompletionsAsync(context) =
asyncMaybe {
let document = context.Document
let position = context.Position
do! let extension = Path.GetExtension document.FilePath
Option.guard (extension = ".fsx" || extension = ".fsscript")
let! ct = liftAsync Async.CancellationToken
let! text = document.GetTextAsync ct
let! text = document.GetTextAsync(ct)
do! Option.guard (isInStringLiteral(text, position))
let line = text.Lines.GetLineFromPosition(position)
let lineText = text.ToString(TextSpan.FromBounds(line.Start, position));
let m = directiveRegex.Match lineText
let lineText = text.ToString(TextSpan.FromBounds(line.Start, position))
do! Option.guard m.Success
let quotedPathGroup = m.Groups.["literal"]
let quotedPath = quotedPathGroup.Value;
let endsWithQuote = PathCompletionUtilities.EndsWithQuote(quotedPath)
do! Option.guard (not (endsWithQuote && (position >= line.Start + m.Length)))
let! completion, quotedPathGroup =
completions |> List.tryPick (fun completion ->
match completion.DirectiveRegex.Match lineText with
| m when m.Success ->
let quotedPathGroup = m.Groups.["literal"]
let endsWithQuote = PathCompletionUtilities.EndsWithQuote(quotedPathGroup.Value)
if endsWithQuote && (position >= line.Start + m.Length) then
None
else
Some (completion, quotedPathGroup)
| _ -> None)
let snapshot = text.FindCorrespondingEditorTextSnapshot()
do! Option.guard (not (isNull snapshot))
let fileSystem = CurrentWorkingDirectoryDiscoveryService.GetService(snapshot)
let extraSearchPaths =
if completion.UseIncludeDirectives then
getIncludeDirectives (text, position)
else []
let defaultSearchPath = Path.GetDirectoryName document.FilePath
let searchPaths = defaultSearchPath :: extraSearchPaths
let helper =
FileSystemCompletionHelper(
provider,
this,
getTextChangeSpan(text, position, quotedPathGroup),
fileSystem,
Glyph.OpenFolder,
allowableExtensions |> List.tryPick getFileGlyph |> Option.defaultValue Glyph.None,
completion.AllowableExtensions |> List.tryPick getFileGlyph |> Option.defaultValue Glyph.None,
searchPaths = Seq.toImmutableArray searchPaths,
allowableExtensions = allowableExtensions,
allowableExtensions = completion.AllowableExtensions,
itemRules = rules)
let pathThroughLastSlash = getPathThroughLastSlash(text, position, quotedPathGroup)
let documentPath = if document.Project.IsSubmission then null else document.FilePath
return helper.GetItems(pathThroughLastSlash, documentPath)
} |> Async.map (Option.defaultValue ImmutableArray.Empty)
let isInsertionTrigger(text: SourceText, position) =
context.AddItems(helper.GetItems(pathThroughLastSlash, documentPath))
}
|> Async.Ignore
|> CommonRoslynHelpers.StartAsyncUnitAsTask context.CancellationToken
override __.IsInsertionTrigger(text, position, _) =
// Bring up completion when the user types a quote (i.e.: #r "), or if they type a slash
// path separator character, or if they type a comma (#r "foo,version...").
// Also, if they're starting a word. i.e. #r "c:\W
let ch = text.[position]
ch = '"' || ch = '\\' || ch = ',' || ch = '/' ||
CommonCompletionUtilities.IsStartingNewWord(text, position, (fun x -> Char.IsLetter x), (fun x -> Char.IsLetterOrDigit x))
let isTriggerChar =
ch = '"' || ch = '\\' || ch = ',' || ch = '/' ||
CommonCompletionUtilities.IsStartingNewWord(text, position, (fun x -> Char.IsLetter x), (fun x -> Char.IsLetterOrDigit x))
isTriggerChar && isInStringLiteral(text, position)
let getTextChange(selectedItem: CompletionItem, ch: Nullable<char>) =
override __.GetTextChangeAsync(selectedItem, ch, cancellationToken) =
// When we commit "\\" when the user types \ we have to adjust for the fact that the
// controller will automatically append \ after we commit. Because of that, we don't
// want to actually commit "\\" as we'll end up with "\\\". So instead we just commit
// "\" and know that controller will append "\" and give us "\\".
if selectedItem.DisplayText = NetworkPath && ch = Nullable '\\' then
Some (TextChange(selectedItem.Span, "\\"))
Task.FromResult(Nullable(TextChange(selectedItem.Span, "\\")))
else
None
let private includeDirectiveCleanRegex = Regex("""#I\s+(@?"*(?<literal>[^"]*)"?)""", RegexOptions.Compiled ||| RegexOptions.ExplicitCapture)
let getIncludeDirectives (document: Document, position: int) =
async {
let! ct = Async.CancellationToken
let! text = document.GetTextAsync(ct)
let lines = text.Lines
let caretLine = text.Lines.GetLinePosition(position).Line
return
lines
|> Seq.filter (fun x -> x.LineNumber <= caretLine)
|> Seq.choose (fun line ->
let lineStr = line.ToString().Trim()
// optimization: fail fast if the line does not start with "(optional spaces) #I"
if not (lineStr.StartsWith "#I") then None
else
match includeDirectiveCleanRegex.Match lineStr with
| m when m.Success -> Some (m.Groups.["literal"].Value)
| _ -> None
)
|> Seq.toList
}
[<AbstractClass>]
type internal HashDirectiveCompletionProvider(directiveRegex: string, allowableExtensions: string list, useIncludeDirectives: bool) =
inherit CommonCompletionProvider()
let directiveRegex = Regex(directiveRegex, RegexOptions.Compiled ||| RegexOptions.ExplicitCapture)
override this.ProvideCompletionsAsync(context) =
async {
let defaultSearchPath = Path.GetDirectoryName context.Document.FilePath
let! extraSearchPaths =
if useIncludeDirectives then
FileSystemCompletion.getIncludeDirectives (context.Document, context.Position)
else async.Return []
let searchPaths = defaultSearchPath :: extraSearchPaths
let! items = FileSystemCompletion.getItems(this, context.Document, context.Position, allowableExtensions, directiveRegex, searchPaths)
context.AddItems(items)
} |> CommonRoslynHelpers.StartAsyncUnitAsTask context.CancellationToken
override __.IsInsertionTrigger(text, position, _) = FileSystemCompletion.isInsertionTrigger(text, position)
override __.GetTextChangeAsync(selectedItem, ch, cancellationToken) =
match FileSystemCompletion.getTextChange(selectedItem, ch) with
| Some x -> Task.FromResult(Nullable x)
| None -> base.GetTextChangeAsync(selectedItem, ch, cancellationToken)
type internal LoadDirectiveCompletionProvider() =
inherit HashDirectiveCompletionProvider("""\s*#load\s+(@?"*(?<literal>"[^"]*"?))""", [".fs"; ".fsx"], useIncludeDirectives = true)
type internal ReferenceDirectiveCompletionProvider() =
inherit HashDirectiveCompletionProvider("""\s*#r\s+(@?"*(?<literal>"[^"]*"?))""", [".dll"; ".exe"], useIncludeDirectives = true)
type internal IncludeDirectiveCompletionProvider() =
// 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.
inherit HashDirectiveCompletionProvider("""\s*#I\s+(@?"*(?<literal>"[^"]*"?))""", [".impossible_extension"], useIncludeDirectives = false)
base.GetTextChangeAsync(selectedItem, ch, cancellationToken)