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:
Родитель
df1276e1e4
Коммит
50838327a4
|
@ -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)
|
Загрузка…
Ссылка в новой задаче