From 58e3753aa0ef808a37860c8d8adb488c2cafd2da Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Mon, 3 Apr 2017 23:19:32 +0300 Subject: [PATCH] Refactor QuickInfoProvider (#2778) * refactor QuickInfoProvider * fix wrong "this symbol range" * further refactoring --- .../CodeFix/AddOpenCodeFixProvider.fs | 2 +- .../ImplementInterfaceCodeFixProvider.fs | 2 +- .../src/FSharp.Editor/Common/RoslynHelpers.fs | 3 +- .../Completion/CompletionProvider.fs | 4 +- .../DocumentHighlightsService.fs | 2 +- .../InlineRename/InlineRenameService.fs | 2 - .../FSharpCheckerExtensions.fs | 4 - .../LanguageService/SymbolHelpers.fs | 2 +- .../LanguageService/Tokenizer.fs | 975 +++++++++--------- .../LanguageService/TypedAstUtils.fs | 3 - .../Navigation/FindUsagesService.fs | 2 +- .../Navigation/GoToDefinitionService.fs | 33 +- .../QuickInfo/QuickInfoProvider.fs | 269 ++--- .../unittests/ColorizationServiceTests.fs | 2 - 14 files changed, 647 insertions(+), 658 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs index d8511fd99..985cf9220 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs @@ -101,7 +101,7 @@ type internal FSharpAddOpenCodeFixProvider let! symbol = asyncMaybe { - let! lexerSymbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, context.Span.End, document.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! lexerSymbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, context.Span.End, document.FilePath, defines, SymbolLookupKind.Greedy) return! checkResults.GetSymbolUseAtLocation(Line.fromZ linePos.Line, lexerSymbol.Ident.idRange.EndColumn, line.ToString(), lexerSymbol.FullIsland) } |> liftAsync diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs index 2f5d48af4..04344a6eb 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs @@ -169,7 +169,7 @@ type internal FSharpImplementInterfaceCodeFixProvider | _ -> Some context.Span.End let! interfaceState = queryInterfaceState appendBracketAt interfacePos tokens parsedInput - let! symbol = Tokenizer.getSymbolAtPosition(context.Document.Id, sourceText, fixupPosition, context.Document.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! symbol = Tokenizer.getSymbolAtPosition(context.Document.Id, sourceText, fixupPosition, context.Document.FilePath, defines, SymbolLookupKind.Greedy) let fcsTextLineNumber = textLine.LineNumber + 1 let lineContents = textLine.ToString() let! options = context.Document.GetOptionsAsync(cancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs index 981cf1826..79a4e6903 100644 --- a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs @@ -13,10 +13,9 @@ open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.Layout open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.FSharp.Compiler.Range -open Microsoft.FSharp.Compiler.Ast -open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.VisualStudio.FSharp.LanguageService +[] module internal RoslynHelpers = let FSharpRangeToTextSpan(sourceText: SourceText, range: range) = diff --git a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs index 22279df5b..edd872629 100644 --- a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs @@ -21,8 +21,6 @@ open Microsoft.VisualStudio.Shell.Interop open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.Range open Microsoft.FSharp.Compiler.SourceCodeServices -open Tokenizer - type internal FSharpCompletionProvider ( @@ -142,7 +140,7 @@ type internal FSharpCompletionProvider let maxHints = if mruItems.Values.Count = 0 then 0 else Seq.max mruItems.Values sortedDeclItems |> Array.iteri (fun number declItem -> - let glyph = FSharpGlyphToRoslynGlyph (declItem.Glyph, declItem.Accessibility) + let glyph = Tokenizer.FSharpGlyphToRoslynGlyph (declItem.Glyph, declItem.Accessibility) let name = match entityKind, declItem.NamespaceToOpen with | Some EntityKind.Attribute, _ when declItem.IsAttribute && declItem.Name.EndsWith "Attribute" -> diff --git a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs index 450b02895..2d37d0253 100644 --- a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs +++ b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs @@ -56,7 +56,7 @@ type internal FSharpDocumentHighlightsService [] (checkerP let textLine = sourceText.Lines.GetLineFromPosition(position) let textLinePos = sourceText.Lines.GetLinePosition(position) let fcsTextLineNumber = Line.fromZ textLinePos.Line - let! symbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! symbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) let! _, _, checkFileResults = checker.ParseAndCheckDocument(filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland) let! symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) |> liftAsync diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index 1ae5d080c..204b37edf 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -17,10 +17,8 @@ open Microsoft.CodeAnalysis.Text open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.Range open Microsoft.FSharp.Compiler.SourceCodeServices -open Tokenizer open Symbols - type internal FailureInlineRenameInfo private () = interface IInlineRenameInfo with member __.CanRename = false diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs index 919b291ba..04d0ca005 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpCheckerExtensions.fs @@ -8,15 +8,11 @@ open Microsoft.CodeAnalysis.Text open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.Ast open Microsoft.FSharp.Compiler.SourceCodeServices -open Tokenizer -open Symbols open TypedAstUtils - type CheckResults = | Ready of (FSharpParseFileResults * FSharpCheckFileResults) option | StillRunning of Async<(FSharpParseFileResults * FSharpCheckFileResults) option> - type FSharpChecker with member this.ParseDocument(document: Document, options: FSharpProjectOptions, sourceText: string) = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index 256ef503b..11b8ad5a8 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -65,7 +65,7 @@ module internal SymbolHelpers = do! Option.guard (originalText.Length > 0) let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) - let! symbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, symbolSpan.Start, document.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! symbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, symbolSpan.Start, document.FilePath, defines, SymbolLookupKind.Greedy) let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) let textLine = sourceText.Lines.GetLineFromPosition(symbolSpan.Start) let textLinePos = sourceText.Lines.GetLinePosition(symbolSpan.Start) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs b/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs index 19fed27cb..9d4cc69a5 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs @@ -1,4 +1,4 @@ -module internal Microsoft.VisualStudio.FSharp.Editor.Tokenizer +namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Collections.Generic @@ -17,7 +17,7 @@ open Microsoft.FSharp.Compiler.SourceCodeServices [] -type LexerSymbolKind = +type internal LexerSymbolKind = | Ident | Operator | Punctuation @@ -25,7 +25,7 @@ type LexerSymbolKind = | StaticallyResolvedTypeParameter | Other -type LexerSymbol = +type internal LexerSymbol = { Kind: LexerSymbolKind /// Last part of `LongIdent` Ident: Ident @@ -34,554 +34,555 @@ type LexerSymbol = member x.Range: Range.range = x.Ident.idRange [] -type SymbolLookupKind = +type internal SymbolLookupKind = /// Position must lay inside symbol range. | Precise /// Position may lay one column outside of symbol range to the right. | Greedy +[] +module internal Tokenizer = + let (|Public|Internal|Protected|Private|) (a: FSharpAccessibility option) = + match a with + | None -> Public + | Some a -> + if a.IsPublic then Public + elif a.IsInternal then Internal + elif a.IsPrivate then Private + else Protected -let inline (|Public|Internal|Protected|Private|) (a: FSharpAccessibility option) = - match a with - | None -> Public - | Some a -> - if a.IsPublic then Public - elif a.IsInternal then Internal - elif a.IsPrivate then Private - else Protected - -let FSharpGlyphToRoslynGlyph (glyph: FSharpGlyph, accessibility: FSharpAccessibility option) = - match glyph with - | FSharpGlyph.Class - | FSharpGlyph.Exception - | FSharpGlyph.Typedef - | FSharpGlyph.Type -> - match accessibility with - | Public -> Glyph.ClassPublic - | Internal -> Glyph.ClassInternal - | Protected -> Glyph.ClassProtected - | Private -> Glyph.ClassPrivate - | FSharpGlyph.Constant -> - match accessibility with - | Public -> Glyph.ConstantPublic - | Internal -> Glyph.ConstantInternal - | Protected -> Glyph.ConstantProtected - | Private -> Glyph.ConstantPrivate - | FSharpGlyph.Delegate -> - match accessibility with - | Public -> Glyph.DelegatePublic - | Internal -> Glyph.DelegateInternal - | Protected -> Glyph.DelegateProtected - | Private -> Glyph.DelegatePrivate - | FSharpGlyph.Enum - | FSharpGlyph.Union -> - match accessibility with - | Public -> Glyph.EnumPublic - | Internal -> Glyph.EnumInternal - | Protected -> Glyph.EnumProtected - | Private -> Glyph.EnumPrivate - | FSharpGlyph.EnumMember -> Glyph.EnumMember - | FSharpGlyph.Event -> - match accessibility with - | Public -> Glyph.EventPublic - | Internal -> Glyph.EventInternal - | Protected -> Glyph.EventProtected - | Private -> Glyph.EventPrivate - | FSharpGlyph.Field -> - match accessibility with - | Public -> Glyph.FieldPublic - | Internal -> Glyph.FieldInternal - | Protected -> Glyph.FieldProtected - | Private -> Glyph.FieldPrivate - | FSharpGlyph.Interface -> - match accessibility with - | Public -> Glyph.InterfacePublic - | Internal -> Glyph.InterfaceInternal - | Protected -> Glyph.InterfaceProtected - | Private -> Glyph.InterfacePrivate - | FSharpGlyph.Method - | FSharpGlyph.OverridenMethod -> - match accessibility with - | Public -> Glyph.MethodPublic - | Internal -> Glyph.MethodInternal - | Protected -> Glyph.MethodProtected - | Private -> Glyph.MethodPrivate - | FSharpGlyph.ExtensionMethod -> - match accessibility with - | Public -> Glyph.ExtensionMethodPublic - | Internal -> Glyph.ExtensionMethodInternal - | Protected -> Glyph.ExtensionMethodProtected - | Private -> Glyph.ExtensionMethodPrivate - | FSharpGlyph.Module -> - match accessibility with - | Public -> Glyph.ModulePublic - | Internal -> Glyph.ModuleInternal - | Protected -> Glyph.ModuleProtected - | Private -> Glyph.ModulePrivate - | FSharpGlyph.NameSpace -> Glyph.Namespace - | FSharpGlyph.Property -> - match accessibility with - | Public -> Glyph.PropertyPublic - | Internal -> Glyph.PropertyInternal - | Protected -> Glyph.PropertyProtected - | Private -> Glyph.PropertyPrivate - | FSharpGlyph.Struct -> - match accessibility with - | Public -> Glyph.StructurePublic - | Internal -> Glyph.StructureInternal - | Protected -> Glyph.StructureProtected - | Private -> Glyph.StructurePrivate - | FSharpGlyph.Variable -> Glyph.Local - | FSharpGlyph.Error -> Glyph.Error - -let GetGlyphForSymbol (symbol: FSharpSymbol, kind: LexerSymbolKind) = - match kind with - | LexerSymbolKind.Operator -> Glyph.Operator - | _ -> - match symbol with - | :? FSharpUnionCase as x -> - match Some x.Accessibility with - | Public -> Glyph.EnumPublic - | Internal -> Glyph.EnumInternal - | Protected -> Glyph.EnumProtected - | Private -> Glyph.EnumPrivate - | :? FSharpActivePatternCase -> Glyph.EnumPublic - | :? FSharpField as x -> - if x.IsLiteral then - match Some x.Accessibility with + let FSharpGlyphToRoslynGlyph (glyph: FSharpGlyph, accessibility: FSharpAccessibility option) = + match glyph with + | FSharpGlyph.Class + | FSharpGlyph.Exception + | FSharpGlyph.Typedef + | FSharpGlyph.Type -> + match accessibility with + | Public -> Glyph.ClassPublic + | Internal -> Glyph.ClassInternal + | Protected -> Glyph.ClassProtected + | Private -> Glyph.ClassPrivate + | FSharpGlyph.Constant -> + match accessibility with | Public -> Glyph.ConstantPublic | Internal -> Glyph.ConstantInternal | Protected -> Glyph.ConstantProtected | Private -> Glyph.ConstantPrivate - else - match Some x.Accessibility with + | FSharpGlyph.Delegate -> + match accessibility with + | Public -> Glyph.DelegatePublic + | Internal -> Glyph.DelegateInternal + | Protected -> Glyph.DelegateProtected + | Private -> Glyph.DelegatePrivate + | FSharpGlyph.Enum + | FSharpGlyph.Union -> + match accessibility with + | Public -> Glyph.EnumPublic + | Internal -> Glyph.EnumInternal + | Protected -> Glyph.EnumProtected + | Private -> Glyph.EnumPrivate + | FSharpGlyph.EnumMember -> Glyph.EnumMember + | FSharpGlyph.Event -> + match accessibility with + | Public -> Glyph.EventPublic + | Internal -> Glyph.EventInternal + | Protected -> Glyph.EventProtected + | Private -> Glyph.EventPrivate + | FSharpGlyph.Field -> + match accessibility with | Public -> Glyph.FieldPublic | Internal -> Glyph.FieldInternal | Protected -> Glyph.FieldProtected | Private -> Glyph.FieldPrivate - | :? FSharpParameter -> Glyph.Parameter - | :? FSharpMemberOrFunctionOrValue as x -> - if x.LiteralValue.IsSome then - match Some x.Accessibility with - | Public -> Glyph.ConstantPublic - | Internal -> Glyph.ConstantInternal - | Protected -> Glyph.ConstantProtected - | Private -> Glyph.ConstantPrivate - elif x.IsExtensionMember then - match Some x.Accessibility with - | Public -> Glyph.ExtensionMethodPublic - | Internal -> Glyph.ExtensionMethodInternal - | Protected -> Glyph.ExtensionMethodProtected - | Private -> Glyph.ExtensionMethodPrivate - elif x.IsProperty || x.IsPropertyGetterMethod || x.IsPropertySetterMethod then - match Some x.Accessibility with - | Public -> Glyph.PropertyPublic - | Internal -> Glyph.PropertyInternal - | Protected -> Glyph.PropertyProtected - | Private -> Glyph.PropertyPrivate - elif x.IsEvent then - match Some x.Accessibility with - | Public -> Glyph.EventPublic - | Internal -> Glyph.EventInternal - | Protected -> Glyph.EventProtected - | Private -> Glyph.EventPrivate - else - match Some x.Accessibility with - | Public -> Glyph.MethodPublic - | Internal -> Glyph.MethodInternal - | Protected -> Glyph.MethodProtected - | Private -> Glyph.MethodPrivate - | :? FSharpEntity as x -> - if x.IsValueType then - match Some x.Accessibility with - | Public -> Glyph.StructurePublic - | Internal -> Glyph.StructureInternal - | Protected -> Glyph.StructureProtected - | Private -> Glyph.StructurePrivate - elif x.IsFSharpModule then - match Some x.Accessibility with - | Public -> Glyph.ModulePublic - | Internal -> Glyph.ModuleInternal - | Protected -> Glyph.ModuleProtected - | Private -> Glyph.ModulePrivate - elif x.IsEnum || x.IsFSharpUnion then + | FSharpGlyph.Interface -> + match accessibility with + | Public -> Glyph.InterfacePublic + | Internal -> Glyph.InterfaceInternal + | Protected -> Glyph.InterfaceProtected + | Private -> Glyph.InterfacePrivate + | FSharpGlyph.Method + | FSharpGlyph.OverridenMethod -> + match accessibility with + | Public -> Glyph.MethodPublic + | Internal -> Glyph.MethodInternal + | Protected -> Glyph.MethodProtected + | Private -> Glyph.MethodPrivate + | FSharpGlyph.ExtensionMethod -> + match accessibility with + | Public -> Glyph.ExtensionMethodPublic + | Internal -> Glyph.ExtensionMethodInternal + | Protected -> Glyph.ExtensionMethodProtected + | Private -> Glyph.ExtensionMethodPrivate + | FSharpGlyph.Module -> + match accessibility with + | Public -> Glyph.ModulePublic + | Internal -> Glyph.ModuleInternal + | Protected -> Glyph.ModuleProtected + | Private -> Glyph.ModulePrivate + | FSharpGlyph.NameSpace -> Glyph.Namespace + | FSharpGlyph.Property -> + match accessibility with + | Public -> Glyph.PropertyPublic + | Internal -> Glyph.PropertyInternal + | Protected -> Glyph.PropertyProtected + | Private -> Glyph.PropertyPrivate + | FSharpGlyph.Struct -> + match accessibility with + | Public -> Glyph.StructurePublic + | Internal -> Glyph.StructureInternal + | Protected -> Glyph.StructureProtected + | Private -> Glyph.StructurePrivate + | FSharpGlyph.Variable -> Glyph.Local + | FSharpGlyph.Error -> Glyph.Error + + let GetGlyphForSymbol (symbol: FSharpSymbol, kind: LexerSymbolKind) = + match kind with + | LexerSymbolKind.Operator -> Glyph.Operator + | _ -> + match symbol with + | :? FSharpUnionCase as x -> match Some x.Accessibility with | Public -> Glyph.EnumPublic | Internal -> Glyph.EnumInternal | Protected -> Glyph.EnumProtected | Private -> Glyph.EnumPrivate - elif x.IsInterface then + | :? FSharpActivePatternCase -> Glyph.EnumPublic + | :? FSharpField as x -> + if x.IsLiteral then match Some x.Accessibility with - | Public -> Glyph.InterfacePublic - | Internal -> Glyph.InterfaceInternal - | Protected -> Glyph.InterfaceProtected - | Private -> Glyph.InterfacePrivate - elif x.IsDelegate then - match Some x.Accessibility with - | Public -> Glyph.DelegatePublic - | Internal -> Glyph.DelegateInternal - | Protected -> Glyph.DelegateProtected - | Private -> Glyph.DelegatePrivate - elif x.IsNamespace then - Glyph.Namespace + | Public -> Glyph.ConstantPublic + | Internal -> Glyph.ConstantInternal + | Protected -> Glyph.ConstantProtected + | Private -> Glyph.ConstantPrivate else match Some x.Accessibility with - | Public -> Glyph.ClassPublic - | Internal -> Glyph.ClassInternal - | Protected -> Glyph.ClassProtected - | Private -> Glyph.ClassPrivate - | _ -> Glyph.None + | Public -> Glyph.FieldPublic + | Internal -> Glyph.FieldInternal + | Protected -> Glyph.FieldProtected + | Private -> Glyph.FieldPrivate + | :? FSharpParameter -> Glyph.Parameter + | :? FSharpMemberOrFunctionOrValue as x -> + if x.LiteralValue.IsSome then + match Some x.Accessibility with + | Public -> Glyph.ConstantPublic + | Internal -> Glyph.ConstantInternal + | Protected -> Glyph.ConstantProtected + | Private -> Glyph.ConstantPrivate + elif x.IsExtensionMember then + match Some x.Accessibility with + | Public -> Glyph.ExtensionMethodPublic + | Internal -> Glyph.ExtensionMethodInternal + | Protected -> Glyph.ExtensionMethodProtected + | Private -> Glyph.ExtensionMethodPrivate + elif x.IsProperty || x.IsPropertyGetterMethod || x.IsPropertySetterMethod then + match Some x.Accessibility with + | Public -> Glyph.PropertyPublic + | Internal -> Glyph.PropertyInternal + | Protected -> Glyph.PropertyProtected + | Private -> Glyph.PropertyPrivate + elif x.IsEvent then + match Some x.Accessibility with + | Public -> Glyph.EventPublic + | Internal -> Glyph.EventInternal + | Protected -> Glyph.EventProtected + | Private -> Glyph.EventPrivate + else + match Some x.Accessibility with + | Public -> Glyph.MethodPublic + | Internal -> Glyph.MethodInternal + | Protected -> Glyph.MethodProtected + | Private -> Glyph.MethodPrivate + | :? FSharpEntity as x -> + if x.IsValueType then + match Some x.Accessibility with + | Public -> Glyph.StructurePublic + | Internal -> Glyph.StructureInternal + | Protected -> Glyph.StructureProtected + | Private -> Glyph.StructurePrivate + elif x.IsFSharpModule then + match Some x.Accessibility with + | Public -> Glyph.ModulePublic + | Internal -> Glyph.ModuleInternal + | Protected -> Glyph.ModuleProtected + | Private -> Glyph.ModulePrivate + elif x.IsEnum || x.IsFSharpUnion then + match Some x.Accessibility with + | Public -> Glyph.EnumPublic + | Internal -> Glyph.EnumInternal + | Protected -> Glyph.EnumProtected + | Private -> Glyph.EnumPrivate + elif x.IsInterface then + match Some x.Accessibility with + | Public -> Glyph.InterfacePublic + | Internal -> Glyph.InterfaceInternal + | Protected -> Glyph.InterfaceProtected + | Private -> Glyph.InterfacePrivate + elif x.IsDelegate then + match Some x.Accessibility with + | Public -> Glyph.DelegatePublic + | Internal -> Glyph.DelegateInternal + | Protected -> Glyph.DelegateProtected + | Private -> Glyph.DelegatePrivate + elif x.IsNamespace then + Glyph.Namespace + else + match Some x.Accessibility with + | Public -> Glyph.ClassPublic + | Internal -> Glyph.ClassInternal + | Protected -> Glyph.ClassProtected + | Private -> Glyph.ClassPrivate + | _ -> Glyph.None -type private SourceLineData(lineStart: int, lexStateAtStartOfLine: FSharpTokenizerLexState, lexStateAtEndOfLine: FSharpTokenizerLexState, - hashCode: int, classifiedSpans: IReadOnlyList, tokens: FSharpTokenInfo list) = - member val LineStart = lineStart - member val LexStateAtStartOfLine = lexStateAtStartOfLine - member val LexStateAtEndOfLine = lexStateAtEndOfLine - member val HashCode = hashCode - member val ClassifiedSpans = classifiedSpans - member val Tokens = tokens + type private SourceLineData(lineStart: int, lexStateAtStartOfLine: FSharpTokenizerLexState, lexStateAtEndOfLine: FSharpTokenizerLexState, + hashCode: int, classifiedSpans: IReadOnlyList, tokens: FSharpTokenInfo list) = + member val LineStart = lineStart + member val LexStateAtStartOfLine = lexStateAtStartOfLine + member val LexStateAtEndOfLine = lexStateAtEndOfLine + member val HashCode = hashCode + member val ClassifiedSpans = classifiedSpans + member val Tokens = tokens - member data.IsValid(textLine: TextLine) = - data.LineStart = textLine.Start && - let lineContents = textLine.Text.ToString(textLine.Span) - data.HashCode = lineContents.GetHashCode() + member data.IsValid(textLine: TextLine) = + data.LineStart = textLine.Start && + let lineContents = textLine.Text.ToString(textLine.Span) + data.HashCode = lineContents.GetHashCode() -type private SourceTextData(approxLines: int) = - let data = ResizeArray(approxLines) - let extendTo i = - if i >= data.Count then - data.Capacity <- i + 1 - for j in data.Count .. i do - data.Add(None) - member x.Item - with get (i:int) = extendTo i; data.[i] - and set (i:int) v = extendTo i; data.[i] <- v + type private SourceTextData(approxLines: int) = + let data = ResizeArray(approxLines) + let extendTo i = + if i >= data.Count then + data.Capacity <- i + 1 + for j in data.Count .. i do + data.Add(None) + member x.Item + with get (i:int) = extendTo i; data.[i] + and set (i:int) v = extendTo i; data.[i] <- v - member x.ClearFrom(n) = - let mutable i = n - while i < data.Count && data.[i].IsSome do - data.[i] <- None - i <- i + 1 + member x.ClearFrom(n) = + let mutable i = n + while i < data.Count && data.[i].IsSome do + data.[i] <- None + i <- i + 1 -let private dataCache = ConditionalWeakTable() + let private dataCache = ConditionalWeakTable() -let compilerTokenToRoslynToken(colorKind: FSharpTokenColorKind) : string = - match colorKind with - | FSharpTokenColorKind.Comment -> ClassificationTypeNames.Comment - | FSharpTokenColorKind.Identifier -> ClassificationTypeNames.Identifier - | FSharpTokenColorKind.Keyword -> ClassificationTypeNames.Keyword - | FSharpTokenColorKind.String -> ClassificationTypeNames.StringLiteral - | FSharpTokenColorKind.Text -> ClassificationTypeNames.Text - | FSharpTokenColorKind.UpperIdentifier -> ClassificationTypeNames.Identifier - | FSharpTokenColorKind.Number -> ClassificationTypeNames.NumericLiteral - | FSharpTokenColorKind.InactiveCode -> ClassificationTypeNames.ExcludedCode - | FSharpTokenColorKind.PreprocessorKeyword -> ClassificationTypeNames.PreprocessorKeyword - | FSharpTokenColorKind.Operator -> ClassificationTypeNames.Operator - | FSharpTokenColorKind.Punctuation -> ClassificationTypeNames.Punctuation - | FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text + let compilerTokenToRoslynToken(colorKind: FSharpTokenColorKind) : string = + match colorKind with + | FSharpTokenColorKind.Comment -> ClassificationTypeNames.Comment + | FSharpTokenColorKind.Identifier -> ClassificationTypeNames.Identifier + | FSharpTokenColorKind.Keyword -> ClassificationTypeNames.Keyword + | FSharpTokenColorKind.String -> ClassificationTypeNames.StringLiteral + | FSharpTokenColorKind.Text -> ClassificationTypeNames.Text + | FSharpTokenColorKind.UpperIdentifier -> ClassificationTypeNames.Identifier + | FSharpTokenColorKind.Number -> ClassificationTypeNames.NumericLiteral + | FSharpTokenColorKind.InactiveCode -> ClassificationTypeNames.ExcludedCode + | FSharpTokenColorKind.PreprocessorKeyword -> ClassificationTypeNames.PreprocessorKeyword + | FSharpTokenColorKind.Operator -> ClassificationTypeNames.Operator + | FSharpTokenColorKind.Punctuation -> ClassificationTypeNames.Punctuation + | FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text -let private scanSourceLine(sourceTokenizer: FSharpSourceTokenizer, textLine: TextLine, lineContents: string, lexState: FSharpTokenizerLexState) : SourceLineData = - let colorMap = Array.create textLine.Span.Length ClassificationTypeNames.Text - let lineTokenizer = sourceTokenizer.CreateLineTokenizer(lineContents) - let tokens = ResizeArray() + let private scanSourceLine(sourceTokenizer: FSharpSourceTokenizer, textLine: TextLine, lineContents: string, lexState: FSharpTokenizerLexState) : SourceLineData = + let colorMap = Array.create textLine.Span.Length ClassificationTypeNames.Text + let lineTokenizer = sourceTokenizer.CreateLineTokenizer(lineContents) + let tokens = ResizeArray() - let scanAndColorNextToken(lineTokenizer: FSharpLineTokenizer, lexState: Ref) : Option = - let tokenInfoOption, nextLexState = lineTokenizer.ScanToken(lexState.Value) - lexState.Value <- nextLexState - if tokenInfoOption.IsSome then - let classificationType = compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass) - for i = tokenInfoOption.Value.LeftColumn to tokenInfoOption.Value.RightColumn do - Array.set colorMap i classificationType - tokens.Add tokenInfoOption.Value - tokenInfoOption + let scanAndColorNextToken(lineTokenizer: FSharpLineTokenizer, lexState: Ref) : Option = + let tokenInfoOption, nextLexState = lineTokenizer.ScanToken(lexState.Value) + lexState.Value <- nextLexState + if tokenInfoOption.IsSome then + let classificationType = compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass) + for i = tokenInfoOption.Value.LeftColumn to tokenInfoOption.Value.RightColumn do + Array.set colorMap i classificationType + tokens.Add tokenInfoOption.Value + tokenInfoOption - let previousLexState = ref lexState - let mutable tokenInfoOption = scanAndColorNextToken(lineTokenizer, previousLexState) - while tokenInfoOption.IsSome do - tokenInfoOption <- scanAndColorNextToken(lineTokenizer, previousLexState) + let previousLexState = ref lexState + let mutable tokenInfoOption = scanAndColorNextToken(lineTokenizer, previousLexState) + while tokenInfoOption.IsSome do + tokenInfoOption <- scanAndColorNextToken(lineTokenizer, previousLexState) - let mutable startPosition = 0 - let mutable endPosition = startPosition - let classifiedSpans = new List() + let mutable startPosition = 0 + let mutable endPosition = startPosition + let classifiedSpans = new List() - while startPosition < colorMap.Length do - let classificationType = colorMap.[startPosition] - endPosition <- startPosition - while endPosition < colorMap.Length && classificationType = colorMap.[endPosition] do - endPosition <- endPosition + 1 - let textSpan = new TextSpan(textLine.Start + startPosition, endPosition - startPosition) - classifiedSpans.Add(new ClassifiedSpan(classificationType, textSpan)) - startPosition <- endPosition + while startPosition < colorMap.Length do + let classificationType = colorMap.[startPosition] + endPosition <- startPosition + while endPosition < colorMap.Length && classificationType = colorMap.[endPosition] do + endPosition <- endPosition + 1 + let textSpan = new TextSpan(textLine.Start + startPosition, endPosition - startPosition) + classifiedSpans.Add(new ClassifiedSpan(classificationType, textSpan)) + startPosition <- endPosition - SourceLineData(textLine.Start, lexState, previousLexState.Value, lineContents.GetHashCode(), classifiedSpans, List.ofSeq tokens) + SourceLineData(textLine.Start, lexState, previousLexState.Value, lineContents.GetHashCode(), classifiedSpans, List.ofSeq tokens) -let getColorizationData(documentKey: DocumentId, sourceText: SourceText, textSpan: TextSpan, fileName: string option, defines: string list, - cancellationToken: CancellationToken) : List = - try - let sourceTokenizer = FSharpSourceTokenizer(defines, fileName) - let lines = sourceText.Lines - // We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines) - let sourceTextData = dataCache.GetValue(documentKey, fun key -> SourceTextData(lines.Count)) + let getColorizationData(documentKey: DocumentId, sourceText: SourceText, textSpan: TextSpan, fileName: string option, defines: string list, + cancellationToken: CancellationToken) : List = + try + let sourceTokenizer = FSharpSourceTokenizer(defines, fileName) + let lines = sourceText.Lines + // We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines) + let sourceTextData = dataCache.GetValue(documentKey, fun key -> SourceTextData(lines.Count)) - let startLine = lines.GetLineFromPosition(textSpan.Start).LineNumber - let endLine = lines.GetLineFromPosition(textSpan.End).LineNumber - // Go backwards to find the last cached scanned line that is valid - let scanStartLine = - let mutable i = startLine - while i > 0 && (match sourceTextData.[i] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do - i <- i - 1 - i - // Rescan the lines if necessary and report the information - let result = new List() - let mutable lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine + let startLine = lines.GetLineFromPosition(textSpan.Start).LineNumber + let endLine = lines.GetLineFromPosition(textSpan.End).LineNumber + // Go backwards to find the last cached scanned line that is valid + let scanStartLine = + let mutable i = startLine + while i > 0 && (match sourceTextData.[i] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do + i <- i - 1 + i + // Rescan the lines if necessary and report the information + let result = new List() + let mutable lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine - for i = scanStartLine to endLine do - cancellationToken.ThrowIfCancellationRequested() - let textLine = lines.[i] - let lineContents = textLine.Text.ToString(textLine.Span) + for i = scanStartLine to endLine do + cancellationToken.ThrowIfCancellationRequested() + let textLine = lines.[i] + let lineContents = textLine.Text.ToString(textLine.Span) - let lineData = - // We can reuse the old data when - // 1. the line starts at the same overall position - // 2. the hash codes match - // 3. the start-of-line lex states are the same - match sourceTextData.[i] with - | Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine = lexState -> - data - | _ -> - // Otherwise, we recompute - let newData = scanSourceLine(sourceTokenizer, textLine, lineContents, lexState) - sourceTextData.[i] <- Some newData - newData + let lineData = + // We can reuse the old data when + // 1. the line starts at the same overall position + // 2. the hash codes match + // 3. the start-of-line lex states are the same + match sourceTextData.[i] with + | Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine = lexState -> + data + | _ -> + // Otherwise, we recompute + let newData = scanSourceLine(sourceTokenizer, textLine, lineContents, lexState) + sourceTextData.[i] <- Some newData + newData - lexState <- lineData.LexStateAtEndOfLine + lexState <- lineData.LexStateAtEndOfLine - if startLine <= i then - result.AddRange(lineData.ClassifiedSpans |> Seq.filter(fun token -> - textSpan.Contains(token.TextSpan.Start) || - textSpan.Contains(token.TextSpan.End - 1) || - (token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End))) + if startLine <= i then + result.AddRange(lineData.ClassifiedSpans |> Seq.filter(fun token -> + textSpan.Contains(token.TextSpan.Start) || + textSpan.Contains(token.TextSpan.End - 1) || + (token.TextSpan.Start <= textSpan.Start && textSpan.End <= token.TextSpan.End))) - // If necessary, invalidate all subsequent lines after endLine - if endLine < lines.Count - 1 then - match sourceTextData.[endLine+1] with - | Some data -> - if data.LexStateAtStartOfLine <> lexState then - sourceTextData.ClearFrom (endLine+1) - | None -> () - result - with - | :? System.OperationCanceledException -> reraise() - | ex -> - Assert.Exception(ex) - List() + // If necessary, invalidate all subsequent lines after endLine + if endLine < lines.Count - 1 then + match sourceTextData.[endLine+1] with + | Some data -> + if data.LexStateAtStartOfLine <> lexState then + sourceTextData.ClearFrom (endLine+1) + | None -> () + result + with + | :? System.OperationCanceledException -> reraise() + | ex -> + Assert.Exception(ex) + List() -type private DraftToken = { - Kind: LexerSymbolKind - Token: FSharpTokenInfo - RightColumn: int -} with - static member inline Create kind token = - { Kind = kind; Token = token; RightColumn = token.LeftColumn + token.FullMatchedLength - 1 } + type private DraftToken = { + Kind: LexerSymbolKind + Token: FSharpTokenInfo + RightColumn: int + } with + static member inline Create kind token = + { Kind = kind; Token = token; RightColumn = token.LeftColumn + token.FullMatchedLength - 1 } -/// Returns symbol at a given position. -let private getSymbolFromTokens (fileName: string, tokens: FSharpTokenInfo list, linePos: LinePosition, lineStr: string, lookupKind: SymbolLookupKind) : LexerSymbol option = - let isIdentifier t = t.CharClass = FSharpTokenCharKind.Identifier - let isOperator t = t.ColorClass = FSharpTokenColorKind.Operator - let isPunctuation t = t.ColorClass = FSharpTokenColorKind.Punctuation + /// Returns symbol at a given position. + let private getSymbolFromTokens (fileName: string, tokens: FSharpTokenInfo list, linePos: LinePosition, lineStr: string, lookupKind: SymbolLookupKind) : LexerSymbol option = + let isIdentifier t = t.CharClass = FSharpTokenCharKind.Identifier + let isOperator t = t.ColorClass = FSharpTokenColorKind.Operator + let isPunctuation t = t.ColorClass = FSharpTokenColorKind.Punctuation - let inline (|GenericTypeParameterPrefix|StaticallyResolvedTypeParameterPrefix|Other|) (token: FSharpTokenInfo) = - if token.Tag = FSharpTokenTag.QUOTE then GenericTypeParameterPrefix - elif token.Tag = FSharpTokenTag.INFIX_AT_HAT_OP then - // The lexer return INFIX_AT_HAT_OP token for both "^" and "@" symbols. - // We have to check the char itself to distinguish one from another. - if token.FullMatchedLength = 1 && lineStr.[token.LeftColumn] = '^' then - StaticallyResolvedTypeParameterPrefix - else Other - else Other + let inline (|GenericTypeParameterPrefix|StaticallyResolvedTypeParameterPrefix|Other|) (token: FSharpTokenInfo) = + if token.Tag = FSharpTokenTag.QUOTE then GenericTypeParameterPrefix + elif token.Tag = FSharpTokenTag.INFIX_AT_HAT_OP then + // The lexer return INFIX_AT_HAT_OP token for both "^" and "@" symbols. + // We have to check the char itself to distinguish one from another. + if token.FullMatchedLength = 1 && lineStr.[token.LeftColumn] = '^' then + StaticallyResolvedTypeParameterPrefix + else Other + else Other - // Operators: Filter out overlapped operators (>>= operator is tokenized as three distinct tokens: GREATER, GREATER, EQUALS. - // Each of them has FullMatchedLength = 3. So, we take the first GREATER and skip the other two). - // - // Generic type parameters: we convert QUOTE + IDENT tokens into single IDENT token, altering its LeftColumn - // and FullMathedLength (for "'type" which is tokenized as (QUOTE, left=2) + (IDENT, left=3, length=4) - // we'll get (IDENT, left=2, length=5). - // - // Statically resolved type parameters: we convert INFIX_AT_HAT_OP + IDENT tokens into single IDENT token, altering its LeftColumn - // and FullMathedLength (for "^type" which is tokenized as (INFIX_AT_HAT_OP, left=2) + (IDENT, left=3, length=4) - // we'll get (IDENT, left=2, length=5). - let tokens = - let tokensCount = tokens.Length - tokens - |> List.foldi (fun (acc, lastToken) index (token: FSharpTokenInfo) -> - match lastToken with - | Some t when token.LeftColumn <= t.RightColumn -> acc, lastToken - | _ -> - let isLastToken = index = tokensCount - 1 - match token with - | GenericTypeParameterPrefix when not isLastToken -> acc, Some (DraftToken.Create LexerSymbolKind.GenericTypeParameter token) - | StaticallyResolvedTypeParameterPrefix when not isLastToken -> acc, Some (DraftToken.Create LexerSymbolKind.StaticallyResolvedTypeParameter token) + // Operators: Filter out overlapped operators (>>= operator is tokenized as three distinct tokens: GREATER, GREATER, EQUALS. + // Each of them has FullMatchedLength = 3. So, we take the first GREATER and skip the other two). + // + // Generic type parameters: we convert QUOTE + IDENT tokens into single IDENT token, altering its LeftColumn + // and FullMathedLength (for "'type" which is tokenized as (QUOTE, left=2) + (IDENT, left=3, length=4) + // we'll get (IDENT, left=2, length=5). + // + // Statically resolved type parameters: we convert INFIX_AT_HAT_OP + IDENT tokens into single IDENT token, altering its LeftColumn + // and FullMathedLength (for "^type" which is tokenized as (INFIX_AT_HAT_OP, left=2) + (IDENT, left=3, length=4) + // we'll get (IDENT, left=2, length=5). + let tokens = + let tokensCount = tokens.Length + tokens + |> List.foldi (fun (acc, lastToken) index (token: FSharpTokenInfo) -> + match lastToken with + | Some t when token.LeftColumn <= t.RightColumn -> acc, lastToken | _ -> - let draftToken = - match lastToken with - | Some { Kind = LexerSymbolKind.GenericTypeParameter | LexerSymbolKind.StaticallyResolvedTypeParameter as kind } when isIdentifier token -> - DraftToken.Create kind { token with LeftColumn = token.LeftColumn - 1 - FullMatchedLength = token.FullMatchedLength + 1 } - // ^ operator - | Some { Kind = LexerSymbolKind.StaticallyResolvedTypeParameter } -> - DraftToken.Create LexerSymbolKind.Operator { token with LeftColumn = token.LeftColumn - 1 - FullMatchedLength = 1 } - | _ -> - let kind = - if isOperator token then LexerSymbolKind.Operator - elif isIdentifier token then LexerSymbolKind.Ident - elif isPunctuation token then LexerSymbolKind.Punctuation - else LexerSymbolKind.Other + let isLastToken = index = tokensCount - 1 + match token with + | GenericTypeParameterPrefix when not isLastToken -> acc, Some (DraftToken.Create LexerSymbolKind.GenericTypeParameter token) + | StaticallyResolvedTypeParameterPrefix when not isLastToken -> acc, Some (DraftToken.Create LexerSymbolKind.StaticallyResolvedTypeParameter token) + | _ -> + let draftToken = + match lastToken with + | Some { Kind = LexerSymbolKind.GenericTypeParameter | LexerSymbolKind.StaticallyResolvedTypeParameter as kind } when isIdentifier token -> + DraftToken.Create kind { token with LeftColumn = token.LeftColumn - 1 + FullMatchedLength = token.FullMatchedLength + 1 } + // ^ operator + | Some { Kind = LexerSymbolKind.StaticallyResolvedTypeParameter } -> + DraftToken.Create LexerSymbolKind.Operator { token with LeftColumn = token.LeftColumn - 1 + FullMatchedLength = 1 } + | _ -> + let kind = + if isOperator token then LexerSymbolKind.Operator + elif isIdentifier token then LexerSymbolKind.Ident + elif isPunctuation token then LexerSymbolKind.Punctuation + else LexerSymbolKind.Other - DraftToken.Create kind token - draftToken :: acc, Some draftToken - ) ([], None) - |> fst + DraftToken.Create kind token + draftToken :: acc, Some draftToken + ) ([], None) + |> fst - // One or two tokens that in touch with the cursor (for "let x|(g) = ()" the tokens will be "x" and "(") - let tokensUnderCursor = - let rightColumnCorrection = - match lookupKind with - | SymbolLookupKind.Precise -> 0 - | SymbolLookupKind.Greedy -> 1 + // One or two tokens that in touch with the cursor (for "let x|(g) = ()" the tokens will be "x" and "(") + let tokensUnderCursor = + let rightColumnCorrection = + match lookupKind with + | SymbolLookupKind.Precise -> 0 + | SymbolLookupKind.Greedy -> 1 - tokens |> List.filter (fun x -> x.Token.LeftColumn <= linePos.Character && (x.RightColumn + rightColumnCorrection) >= linePos.Character) + tokens |> List.filter (fun x -> x.Token.LeftColumn <= linePos.Character && (x.RightColumn + rightColumnCorrection) >= linePos.Character) - // Select IDENT token. If failed, select OPERATOR token. - tokensUnderCursor - |> List.tryFind (fun { DraftToken.Kind = k } -> - match k with - | LexerSymbolKind.Ident - | LexerSymbolKind.GenericTypeParameter - | LexerSymbolKind.StaticallyResolvedTypeParameter -> true - | _ -> false) - |> Option.orElseWith (fun _ -> tokensUnderCursor |> List.tryFind (fun { DraftToken.Kind = k } -> k = LexerSymbolKind.Operator)) - |> Option.map (fun token -> - let plid, _ = QuickParse.GetPartialLongNameEx(lineStr, token.RightColumn) - let identStr = lineStr.Substring(token.Token.LeftColumn, token.Token.FullMatchedLength) - { Kind = token.Kind - Ident = - Ident(identStr, - Range.mkRange - fileName - (Range.mkPos (linePos.Line + 1) token.Token.LeftColumn) - (Range.mkPos (linePos.Line + 1) (token.RightColumn + 1))) - FullIsland = plid @ [identStr] }) + // Select IDENT token. If failed, select OPERATOR token. + tokensUnderCursor + |> List.tryFind (fun { DraftToken.Kind = k } -> + match k with + | LexerSymbolKind.Ident + | LexerSymbolKind.GenericTypeParameter + | LexerSymbolKind.StaticallyResolvedTypeParameter -> true + | _ -> false) + |> Option.orElseWith (fun _ -> tokensUnderCursor |> List.tryFind (fun { DraftToken.Kind = k } -> k = LexerSymbolKind.Operator)) + |> Option.map (fun token -> + let plid, _ = QuickParse.GetPartialLongNameEx(lineStr, token.RightColumn) + let identStr = lineStr.Substring(token.Token.LeftColumn, token.Token.FullMatchedLength) + { Kind = token.Kind + Ident = + Ident(identStr, + Range.mkRange + fileName + (Range.mkPos (linePos.Line + 1) token.Token.LeftColumn) + (Range.mkPos (linePos.Line + 1) (token.RightColumn + 1))) + FullIsland = plid @ [identStr] }) -let private getCachedSourceLineData(documentKey: DocumentId, sourceText: SourceText, position: int, fileName: string, defines: string list) = - let textLine = sourceText.Lines.GetLineFromPosition(position) - let textLinePos = sourceText.Lines.GetLinePosition(position) - let lineNumber = textLinePos.Line + 1 // FCS line number - let sourceTokenizer = FSharpSourceTokenizer(defines, Some fileName) - let lines = sourceText.Lines - // We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines) - let sourceTextData = dataCache.GetValue(documentKey, fun key -> SourceTextData(lines.Count)) - // Go backwards to find the last cached scanned line that is valid - let scanStartLine = - let mutable i = min (lines.Count - 1) lineNumber - while i > 0 && - (match sourceTextData.[i] with - | Some data -> not (data.IsValid(lines.[i])) - | None -> true - ) do - i <- i - 1 - i - let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine - let lineContents = textLine.Text.ToString(textLine.Span) + let private getCachedSourceLineData(documentKey: DocumentId, sourceText: SourceText, position: int, fileName: string, defines: string list) = + let textLine = sourceText.Lines.GetLineFromPosition(position) + let textLinePos = sourceText.Lines.GetLinePosition(position) + let lineNumber = textLinePos.Line + 1 // FCS line number + let sourceTokenizer = FSharpSourceTokenizer(defines, Some fileName) + let lines = sourceText.Lines + // We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines) + let sourceTextData = dataCache.GetValue(documentKey, fun key -> SourceTextData(lines.Count)) + // Go backwards to find the last cached scanned line that is valid + let scanStartLine = + let mutable i = min (lines.Count - 1) lineNumber + while i > 0 && + (match sourceTextData.[i] with + | Some data -> not (data.IsValid(lines.[i])) + | None -> true + ) do + i <- i - 1 + i + let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine + let lineContents = textLine.Text.ToString(textLine.Span) - // We can reuse the old data when - // 1. the line starts at the same overall position - // 2. the hash codes match - // 3. the start-of-line lex states are the same - match sourceTextData.[lineNumber] with - | Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine = lexState -> - data, textLinePos, lineContents - | _ -> - // Otherwise, we recompute - let newData = scanSourceLine(sourceTokenizer, textLine, lineContents, lexState) - sourceTextData.[lineNumber] <- Some newData - newData, textLinePos, lineContents + // We can reuse the old data when + // 1. the line starts at the same overall position + // 2. the hash codes match + // 3. the start-of-line lex states are the same + match sourceTextData.[lineNumber] with + | Some data when data.IsValid(textLine) && data.LexStateAtStartOfLine = lexState -> + data, textLinePos, lineContents + | _ -> + // Otherwise, we recompute + let newData = scanSourceLine(sourceTokenizer, textLine, lineContents, lexState) + sourceTextData.[lineNumber] <- Some newData + newData, textLinePos, lineContents -let tokenizeLine (documentKey, sourceText, position, fileName, defines) = - try - let lineData, _, _ = getCachedSourceLineData(documentKey, sourceText, position, fileName, defines) - lineData.Tokens - with - | ex -> - Assert.Exception(ex) - [] + let tokenizeLine (documentKey, sourceText, position, fileName, defines) = + try + let lineData, _, _ = getCachedSourceLineData(documentKey, sourceText, position, fileName, defines) + lineData.Tokens + with + | ex -> + Assert.Exception(ex) + [] -let getSymbolAtPosition(documentKey: DocumentId, sourceText: SourceText, position: int, fileName: string, defines: string list, lookupKind: SymbolLookupKind) : LexerSymbol option = - try - let lineData, textLinePos, lineContents = getCachedSourceLineData(documentKey, sourceText, position, fileName, defines) - getSymbolFromTokens(fileName, lineData.Tokens, textLinePos, lineContents, lookupKind) - with - | :? System.OperationCanceledException -> reraise() - | ex -> - Assert.Exception(ex) - None + let getSymbolAtPosition(documentKey: DocumentId, sourceText: SourceText, position: int, fileName: string, defines: string list, lookupKind: SymbolLookupKind) : LexerSymbol option = + try + let lineData, textLinePos, lineContents = getCachedSourceLineData(documentKey, sourceText, position, fileName, defines) + getSymbolFromTokens(fileName, lineData.Tokens, textLinePos, lineContents, lookupKind) + with + | :? System.OperationCanceledException -> reraise() + | ex -> + Assert.Exception(ex) + None -/// Fix invalid span if it appears to have redundant suffix and prefix. -let fixupSpan (sourceText: SourceText, span: TextSpan) : TextSpan = - let text = sourceText.GetSubText(span).ToString() - match text.LastIndexOf '.' with - | -1 | 0 -> span - | index -> TextSpan(span.Start + index + 1, text.Length - index - 1) + /// Fix invalid span if it appears to have redundant suffix and prefix. + let fixupSpan (sourceText: SourceText, span: TextSpan) : TextSpan = + let text = sourceText.GetSubText(span).ToString() + match text.LastIndexOf '.' with + | -1 | 0 -> span + | index -> TextSpan(span.Start + index + 1, text.Length - index - 1) -let isValidNameForSymbol (lexerSymbolKind: LexerSymbolKind, symbol: FSharpSymbol, name: string) : bool = - let doubleBackTickDelimiter = "``" + let isValidNameForSymbol (lexerSymbolKind: LexerSymbolKind, symbol: FSharpSymbol, name: string) : bool = + let doubleBackTickDelimiter = "``" - let isDoubleBacktickIdent (s: string) = - let doubledDelimiter = 2 * doubleBackTickDelimiter.Length - if s.StartsWith(doubleBackTickDelimiter) && s.EndsWith(doubleBackTickDelimiter) && s.Length > doubledDelimiter then - let inner = s.Substring(doubleBackTickDelimiter.Length, s.Length - doubledDelimiter) - not (inner.Contains(doubleBackTickDelimiter)) - else false + let isDoubleBacktickIdent (s: string) = + let doubledDelimiter = 2 * doubleBackTickDelimiter.Length + if s.StartsWith(doubleBackTickDelimiter) && s.EndsWith(doubleBackTickDelimiter) && s.Length > doubledDelimiter then + let inner = s.Substring(doubleBackTickDelimiter.Length, s.Length - doubledDelimiter) + not (inner.Contains(doubleBackTickDelimiter)) + else false - let isIdentifier (ident: string) = - if isDoubleBacktickIdent ident then - true - else - ident - |> Seq.mapi (fun i c -> i, c) - |> Seq.forall (fun (i, c) -> - if i = 0 then PrettyNaming.IsIdentifierFirstCharacter c - else PrettyNaming.IsIdentifierPartCharacter c) + let isIdentifier (ident: string) = + if isDoubleBacktickIdent ident then + true + else + ident + |> Seq.mapi (fun i c -> i, c) + |> Seq.forall (fun (i, c) -> + if i = 0 then PrettyNaming.IsIdentifierFirstCharacter c + else PrettyNaming.IsIdentifierPartCharacter c) - let isFixableIdentifier (s: string) = - not (String.IsNullOrEmpty s) && Lexhelp.Keywords.NormalizeIdentifierBackticks s |> isIdentifier + let isFixableIdentifier (s: string) = + not (String.IsNullOrEmpty s) && Lexhelp.Keywords.NormalizeIdentifierBackticks s |> isIdentifier - let forbiddenChars = [| '.'; '+'; '$'; '&'; '['; ']'; '/'; '\\'; '*'; '\'' |] + let forbiddenChars = [| '.'; '+'; '$'; '&'; '['; ']'; '/'; '\\'; '*'; '\'' |] - let isTypeNameIdent (s: string) = - not (String.IsNullOrEmpty s) && s.IndexOfAny forbiddenChars = -1 && isFixableIdentifier s + let isTypeNameIdent (s: string) = + not (String.IsNullOrEmpty s) && s.IndexOfAny forbiddenChars = -1 && isFixableIdentifier s - let isUnionCaseIdent (s: string) = - isTypeNameIdent s && Char.IsUpper(s.Replace(doubleBackTickDelimiter, "").[0]) + let isUnionCaseIdent (s: string) = + isTypeNameIdent s && Char.IsUpper(s.Replace(doubleBackTickDelimiter, "").[0]) - let isTypeParameter (prefix: char) (s: string) = - s.Length >= 2 && s.[0] = prefix && isIdentifier s.[1..] + let isTypeParameter (prefix: char) (s: string) = + s.Length >= 2 && s.[0] = prefix && isIdentifier s.[1..] - let isGenericTypeParameter = isTypeParameter ''' - let isStaticallyResolvedTypeParameter = isTypeParameter '^' + let isGenericTypeParameter = isTypeParameter ''' + let isStaticallyResolvedTypeParameter = isTypeParameter '^' - match lexerSymbolKind, symbol with - | _, :? FSharpUnionCase -> isUnionCaseIdent name - | _, :? FSharpActivePatternCase -> - // Different from union cases, active patterns don't accept double-backtick identifiers - isFixableIdentifier name && not (String.IsNullOrEmpty name) && Char.IsUpper(name.[0]) - | LexerSymbolKind.Operator, _ -> PrettyNaming.IsOperatorName name - | LexerSymbolKind.Punctuation, _ -> PrettyNaming.IsPunctuation name - | LexerSymbolKind.GenericTypeParameter, _ -> isGenericTypeParameter name - | LexerSymbolKind.StaticallyResolvedTypeParameter, _ -> isStaticallyResolvedTypeParameter name - | (LexerSymbolKind.Ident | LexerSymbolKind.Other), _ -> - match symbol with - | :? FSharpEntity as e when e.IsClass || e.IsFSharpRecord || e.IsFSharpUnion || e.IsValueType || e.IsFSharpModule || e.IsInterface -> isTypeNameIdent name - | _ -> isFixableIdentifier name + match lexerSymbolKind, symbol with + | _, :? FSharpUnionCase -> isUnionCaseIdent name + | _, :? FSharpActivePatternCase -> + // Different from union cases, active patterns don't accept double-backtick identifiers + isFixableIdentifier name && not (String.IsNullOrEmpty name) && Char.IsUpper(name.[0]) + | LexerSymbolKind.Operator, _ -> PrettyNaming.IsOperatorName name + | LexerSymbolKind.Punctuation, _ -> PrettyNaming.IsPunctuation name + | LexerSymbolKind.GenericTypeParameter, _ -> isGenericTypeParameter name + | LexerSymbolKind.StaticallyResolvedTypeParameter, _ -> isStaticallyResolvedTypeParameter name + | (LexerSymbolKind.Ident | LexerSymbolKind.Other), _ -> + match symbol with + | :? FSharpEntity as e when e.IsClass || e.IsFSharpRecord || e.IsFSharpUnion || e.IsValueType || e.IsFSharpModule || e.IsInterface -> isTypeNameIdent name + | _ -> isFixableIdentifier name diff --git a/vsintegration/src/FSharp.Editor/LanguageService/TypedAstUtils.fs b/vsintegration/src/FSharp.Editor/LanguageService/TypedAstUtils.fs index 93bf5f391..b857ac6c5 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/TypedAstUtils.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/TypedAstUtils.fs @@ -1,11 +1,9 @@ module internal Microsoft.VisualStudio.FSharp.Editor.TypedAstUtils open System -open Microsoft.FSharp.Compiler.Ast open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.VisualStudio.FSharp.Editor - open System.Text.RegularExpressions let isAttribute<'T> (attribute: FSharpAttribute) = @@ -43,7 +41,6 @@ let private UnnamedUnionFieldRegex = Regex("^Item(\d+)?$", RegexOptions.Compiled let isUnnamedUnionCaseField (field: FSharpField) = UnnamedUnionFieldRegex.IsMatch(field.Name) - module TypedAstPatterns = let (|AbbreviatedType|_|) (entity: FSharpEntity) = diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index d1f05b859..c418ce920 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -54,7 +54,7 @@ type internal FSharpFindUsagesService let lineNumber = sourceText.Lines.GetLinePosition(position).Line + 1 let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.FilePath, options.OtherOptions |> Seq.toList) - let! symbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, position, document.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! symbol = Tokenizer.getSymbolAtPosition(document.Id, sourceText, position, document.FilePath, defines, SymbolLookupKind.Greedy) let! symbolUse = checkFileResults.GetSymbolUseAtLocation(lineNumber, symbol.Ident.idRange.EndColumn, textLine, symbol.FullIsland) let! declaration = checkFileResults.GetDeclarationLocationAlternate (lineNumber, symbol.Ident.idRange.EndColumn, textLine, symbol.FullIsland, false) |> liftAsync let tags = GlyphTags.GetTags(Tokenizer.GetGlyphForSymbol (symbolUse.Symbol, symbol.Kind)) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index 9eac1cf2b..c018b1568 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -45,7 +45,7 @@ module internal FSharpGoToDefinition = let textLine = sourceText.Lines.GetLineFromPosition position let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line - let! lexerSymbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! lexerSymbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) let! _, _, checkFileResults = checker.ParseAndCheckDocument (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = preferSignature) @@ -82,22 +82,17 @@ module internal FSharpGoToDefinition = asyncMaybe { let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject originDocument let defines = CompilerEnvironment.GetCompilationDefinesForEditing (originDocument.FilePath, projectOptions.OtherOptions |> Seq.toList) - let originTextSpan = RoslynHelpers.FSharpRangeToTextSpan (sourceText, originRange) let position = originTextSpan.Start - - let! lexerSymbol = - Tokenizer.getSymbolAtPosition - (originDocument.Id, sourceText, position, originDocument.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) - + let! lexerSymbol = Tokenizer.getSymbolAtPosition(originDocument.Id, sourceText, position, originDocument.FilePath, defines, SymbolLookupKind.Greedy) let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() let! _, _, checkFileResults = checker.ParseAndCheckDocument (originDocument,projectOptions,allowStaleResults=true,sourceText=sourceText) + let idRange = lexerSymbol.Ident.idRange - let! fsSymbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) let symbol = fsSymbolUse.Symbol // if the tooltip was spawned in an implementation file and we have a range targeting @@ -109,10 +104,7 @@ module internal FSharpGoToDefinition = let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath let! implSourceText = implDoc.GetTextAsync () let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject implDoc - let! _, _, checkFileResults = - checker.ParseAndCheckDocument (implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText) - - + let! _, _, checkFileResults = checker.ParseAndCheckDocument (implDoc, projectOptions, allowStaleResults=true, sourceText=implSourceText) let! symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol |> liftAsync let! implSymbol = symbolUses |> Array.tryHead let implTextSpan = RoslynHelpers.FSharpRangeToTextSpan (implSourceText, implSymbol.RangeAlternate) @@ -127,12 +119,10 @@ module internal FSharpGoToDefinition = (targetDocument:Document, symbolRange:range, targetSource:SourceText, checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = findSymbolHelper (targetDocument, symbolRange, targetSource,true, checker, projectInfoManager) - /// find the definition location (implementation file/.fs) of the target symbol let findDefinitionOfSymbolAtRange (targetDocument:Document, symbolRange:range, targetSourceText:SourceText, checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = findSymbolHelper (targetDocument, symbolRange, targetSourceText,false, checker, projectInfoManager) - /// use the targetSymbol to find the first instance of its presence in the provided source file let findSymbolDeclarationInFile @@ -148,7 +138,6 @@ module internal FSharpGoToDefinition = return implSymbol.RangeAlternate } - open FSharpGoToDefinition [] @@ -213,7 +202,6 @@ type internal FSharpGoToDefinitionService [] clearStatusBarAfter 4000 true - /// Navigate to the positon of the textSpan in the provided document // used by quickinfo link navigation when the tooltip contains the correct destination range member this.TryNavigateToTextSpan (document:Document, textSpan:TextSpan) = @@ -227,7 +215,6 @@ type internal FSharpGoToDefinitionService [] clearStatusBarAfter 4000 false - /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition member this.NavigateToSymbolDeclarationAsync (targetDocument:Document, targetSourceText:SourceText, symbolRange:range) = async { @@ -236,7 +223,6 @@ type internal FSharpGoToDefinitionService [] (targetDocument, symbolRange, targetSourceText, checkerProvider.Checker, projectInfoManager) return tryNavigateToItem navresult } - /// find the definition location (implementation file/.fs) of the target symbol member this.NavigateToSymbolDefinitionAsync (targetDocument:Document, targetSourceText:SourceText, symbolRange:range)= async{ @@ -246,14 +232,13 @@ type internal FSharpGoToDefinitionService [] return tryNavigateToItem navresult } - static member FindDefinition (checker: FSharpChecker, documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, defines: string list, options: FSharpProjectOptions, textVersionHash: int) : Option = maybe { let textLine = sourceText.Lines.GetLineFromPosition position let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line - let! lexerSymbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, Tokenizer.SymbolLookupKind.Greedy) + let! lexerSymbol = Tokenizer.getSymbolAtPosition(documentKey, sourceText, position, filePath, defines, SymbolLookupKind.Greedy) let! _, _, checkFileResults = checker.ParseAndCheckDocument (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) |> Async.RunSynchronously @@ -267,7 +252,6 @@ type internal FSharpGoToDefinitionService [] | _ -> return! None } - /// Construct a task that will return a navigation target for the implementation definition of the symbol /// at the provided position in the document member this.FindDefinitionsTask (originDocument:Document, position:int, cancellationToken:CancellationToken) = @@ -288,7 +272,7 @@ type internal FSharpGoToDefinitionService [] let! lexerSymbol = Tokenizer.getSymbolAtPosition - (originDocument.Id, sourceText, position,originDocument.FilePath, defines, Tokenizer.SymbolLookupKind.Greedy) + (originDocument.Id, sourceText, position,originDocument.FilePath, defines, SymbolLookupKind.Greedy) let idRange = lexerSymbol.Ident.idRange let! declarations = @@ -362,13 +346,10 @@ type internal FSharpGoToDefinitionService [] } |> Async.map (Option.defaultValue Seq.empty) |> RoslynHelpers.StartAsyncAsTask cancellationToken - interface IGoToDefinitionService with - // used for 'definition peek' member this.FindDefinitionsAsync (document: Document, position: int, cancellationToken: CancellationToken) = this.FindDefinitionsTask (document, position, cancellationToken) - // used for 'goto definition' proper /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument @@ -413,4 +394,4 @@ type internal FSharpGoToDefinitionService [] stopSearchAnimation () statusBarMessage "Could Not Navigate to Definition of Symbol Under Caret" clearStatusBarAfter 4000 - true // we always return true to prevent the dialog box from appearing + true // we always return true to prevent the dialog box from appearing \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs index 38ea13249..d27341a11 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs @@ -10,6 +10,7 @@ open System.Windows.Controls open System.Windows.Data open System.Windows.Media open System.ComponentModel.Composition +open System.Text open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Classification @@ -29,11 +30,8 @@ open Microsoft.VisualStudio.Language.Intellisense open Microsoft.FSharp.Compiler.SourceCodeServices open Microsoft.FSharp.Compiler.Range open Microsoft.FSharp.Compiler -open Internal.Utilities.StructuredFormat -open Tokenizer -open System.Text -open RoslynHelpers +open Internal.Utilities.StructuredFormat module private SessionHandling = let mutable currentSession = None @@ -53,19 +51,17 @@ type internal SourceLink(run) as this = inherit Documents.Hyperlink(run) let lessOpacity = - { new IValueConverter with - member this.Convert(value, targetType, _, _) = - match value with - | :? Color as c when targetType = typeof -> - // return same color but slightly transparent - Color.FromArgb(70uy, c.R, c.G, c.B) :> _ - | _ -> DependencyProperty.UnsetValue - member this.ConvertBack(_,_,_,_) = DependencyProperty.UnsetValue } + { new IValueConverter with + member this.Convert(value, targetType, _, _) = + match value with + | :? Color as c when targetType = typeof -> + // return same color but slightly transparent + Color.FromArgb(70uy, c.R, c.G, c.B) :> _ + | _ -> DependencyProperty.UnsetValue + member this.ConvertBack(_,_,_,_) = DependencyProperty.UnsetValue } let underlineBrush = Media.SolidColorBrush() - do BindingOperations.SetBinding(underlineBrush, SolidColorBrush.ColorProperty, Binding("Foreground.Color", Source = this, Converter = lessOpacity)) |> ignore - let normalUnderline = TextDecorationCollection [TextDecoration(Location = TextDecorationLocation.Underline, PenOffset = 1.0)] let slightUnderline = TextDecorationCollection [TextDecoration(Location = TextDecorationLocation.Underline, PenOffset = 1.0, Pen = Pen(Brush = underlineBrush))] do this.TextDecorations <- slightUnderline @@ -73,66 +69,83 @@ type internal SourceLink(run) as this = override this.OnMouseEnter(e) = base.OnMouseEnter(e) this.TextDecorations <- normalUnderline + override this.OnMouseLeave(e) = base.OnMouseLeave(e) this.TextDecorations <- slightUnderline -module private FSharpQuickInfo = +type private TooltipInfo = + { StructuredText: FSharpStructuredToolTipText + Span: TextSpan + Symbol: FSharpSymbol + SymbolKind: LexerSymbolKind } - let tooltip (symbolGlyph, mainDescription, documentation) = - let empty = - { new IDeferredQuickInfoContent with - member x.Create() = TextBlock(Visibility = Visibility.Collapsed) :> FrameworkElement } +module private FSharpQuickInfo = + + let private empty = + { new IDeferredQuickInfoContent with + member x.Create() = TextBlock(Visibility = Visibility.Collapsed) :> FrameworkElement } + let createDeferredContent (symbolGlyph, mainDescription, documentation) = QuickInfoDisplayDeferredContent(symbolGlyph, null, mainDescription, documentation, empty, empty, empty, empty) - /// Get tooltip combined from doccom of Signature and definition - let getCompoundTooltipInfo (checker: FSharpChecker, position: int, document: Document, projectInfoManager: ProjectInfoManager, cancellationToken) = + // when a construct has been declared in a signature file the documentation comments that are + // written in that file are the ones that go into the generated xml when the project is compiled + // therefore we should include these doccoms in our design time tooltips + let getTooltipFromRange + ( + checker: FSharpChecker, + projectInfoManager: ProjectInfoManager, + document: Document, + declRange: range, + cancellationToken: CancellationToken + ) + : Async = + asyncMaybe { let solution = document.Project.Solution + // ascertain the location of the target declaration in the signature file + let! extDocId = solution.GetDocumentIdsWithFilePath declRange.FileName |> Seq.tryHead + let extDocument = solution.GetProject(extDocId.ProjectId).GetDocument extDocId + let! extSourceText = extDocument.GetTextAsync cancellationToken + let extSpan = RoslynHelpers.FSharpRangeToTextSpan (extSourceText, declRange) + let extLineText = (extSourceText.Lines.GetLineFromPosition extSpan.Start).ToString() - // when a construct has been declared in a signature file the documentation comments that are - // written in that file are the ones that go into the generated xml when the project is compiled - // therefore we should include these doccoms in our design time tooltips - let getTooltipFromRange (declRange: range) = - asyncMaybe { - // ascertain the location of the target declaration in the signature file - let! extDocId = solution.GetDocumentIdsWithFilePath declRange.FileName |> Seq.tryHead - let extDocument = solution.GetProject(extDocId.ProjectId).GetDocument extDocId - let! extSourceText = extDocument.GetTextAsync cancellationToken - - let extSpan = RoslynHelpers.FSharpRangeToTextSpan (extSourceText, declRange) - let extLineText = (extSourceText.Lines.GetLineFromPosition extSpan.Start).ToString() - - // project options need to be retrieved because the signature file could be in another project - let extProjectOptions = projectInfoManager.TryGetOptionsForProject extDocId.ProjectId |> Option.get - let extDefines = - CompilerEnvironment.GetCompilationDefinesForEditing - (extDocument.FilePath, extProjectOptions.OtherOptions |> Seq.toList) - - let! extLexerSymbol = - Tokenizer.getSymbolAtPosition - (extDocId, extSourceText, extSpan.Start, declRange.FileName, extDefines, SymbolLookupKind.Greedy) - - let! _, _, extCheckFileResults = - checker.ParseAndCheckDocument (extDocument,extProjectOptions,allowStaleResults=true,sourceText=extSourceText) - - let! extTooltipText = - extCheckFileResults.GetStructuredToolTipTextAlternate - (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync - - match extTooltipText with - | FSharpToolTipText [] - | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> - return! None - | extTooltipText -> - let! extSymbolUse = - extCheckFileResults.GetSymbolUseAtLocation(declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland) - - let extTextSpan = RoslynHelpers.FSharpRangeToTextSpan (extSourceText, extLexerSymbol.Range) - return! Some (extTooltipText, extTextSpan, extSymbolUse, extLexerSymbol.Kind) - } + // project options need to be retrieved because the signature file could be in another project + let! extProjectOptions = projectInfoManager.TryGetOptionsForProject extDocId.ProjectId + let extDefines = CompilerEnvironment.GetCompilationDefinesForEditing (extDocument.FilePath, List.ofSeq extProjectOptions.OtherOptions) + let! extLexerSymbol = Tokenizer.getSymbolAtPosition(extDocId, extSourceText, extSpan.Start, declRange.FileName, extDefines, SymbolLookupKind.Greedy) + let! _, _, extCheckFileResults = checker.ParseAndCheckDocument(extDocument,extProjectOptions,allowStaleResults=true,sourceText=extSourceText) + let! extTooltipText = + extCheckFileResults.GetStructuredToolTipTextAlternate + (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) |> liftAsync + + match extTooltipText with + | FSharpToolTipText [] + | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> return! None + | extTooltipText -> + let! extSymbolUse = + extCheckFileResults.GetSymbolUseAtLocation(declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland) + + return { StructuredText = extTooltipText + Span = RoslynHelpers.FSharpRangeToTextSpan (extSourceText, extLexerSymbol.Range) + Symbol = extSymbolUse.Symbol + SymbolKind = extLexerSymbol.Kind } + } + + /// Get tooltip combined from doccom of Signature and definition + let getTooltipInfo + ( + checker: FSharpChecker, + projectInfoManager: ProjectInfoManager, + document: Document, + position: int, + cancellationToken: CancellationToken + ) + : Async<(FSharpSymbolUse * TooltipInfo option * TooltipInfo option) option> = + + asyncMaybe { let! sourceText = document.GetTextAsync cancellationToken let! projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.FilePath, projectOptions.OtherOptions |> Seq.toList) @@ -142,6 +155,7 @@ module private FSharpQuickInfo = let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) /// Gets the tooltip information for the orignal target let getTargetSymbolTooltip () = @@ -154,30 +168,32 @@ module private FSharpQuickInfo = | FSharpToolTipText [] | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> return! None | _ -> - let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) let targetTextSpan = RoslynHelpers.FSharpRangeToTextSpan (sourceText, lexerSymbol.Range) - return! Some (targetTooltip, targetTextSpan, symbolUse, lexerSymbol.Kind) + return { StructuredText = targetTooltip + Span = targetTextSpan + Symbol = symbolUse.Symbol + SymbolKind = lexerSymbol.Kind } } // if the target is in a signature file, adjusting the tooltip info is unnecessary if isSignatureFile document.FilePath then let! targetTooltipInfo = getTargetSymbolTooltip() - return (None ,Some targetTooltipInfo) + return symbolUse, None, Some targetTooltipInfo else // find the declaration location of the target symbol, with a preference for signature files let! findSigDeclarationResult = checkFileResults.GetDeclarationLocationAlternate - (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=true) |> liftAsync + (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=true) |> liftAsync // it is necessary to retrieve the backup tooltip info because this acquires // the textSpan designating where we want the tooltip to appear. - let! backupTooltipInfo & (_, targetTextSpan, _, _) = getTargetSymbolTooltip() + let! targetTooltipInfo = getTargetSymbolTooltip() match findSigDeclarationResult with - | FSharpFindDeclResult.DeclNotFound _failReason -> return None, Some backupTooltipInfo + | FSharpFindDeclResult.DeclNotFound _ -> return symbolUse, None, Some targetTooltipInfo | FSharpFindDeclResult.DeclFound declRange -> if isSignatureFile declRange.FileName then - let! sigTooltipInfo = getTooltipFromRange declRange + let! sigTooltipInfo = getTooltipFromRange(checker, projectInfoManager, document, declRange, cancellationToken) // if the target was declared in a signature file, and the current file // is not the corresponding module implementation file for that signature, // the doccoms from the signature will overwrite any doccoms that might be @@ -188,12 +204,12 @@ module private FSharpQuickInfo = (idRange.StartLine, idRange.EndColumn, lineText, lexerSymbol.FullIsland, preferFlag=false) |> liftAsync match findImplDefinitionResult with - | FSharpFindDeclResult.DeclNotFound _failReason -> return (Some sigTooltipInfo , None) + | FSharpFindDeclResult.DeclNotFound _ -> return symbolUse, Some sigTooltipInfo, None | FSharpFindDeclResult.DeclFound declRange -> - let! (implTooltip, _, implSymbol, implLex) = getTooltipFromRange declRange - return (Some sigTooltipInfo, Some (implTooltip, targetTextSpan, implSymbol, implLex)) + let! implTooltipInfo = getTooltipFromRange(checker, projectInfoManager, document, declRange, cancellationToken) + return symbolUse, Some sigTooltipInfo, Some { implTooltipInfo with Span = targetTooltipInfo.Span } else - return (None, Some backupTooltipInfo) + return symbolUse, None, Some targetTooltipInfo } [] @@ -239,34 +255,37 @@ type internal FSharpQuickInfoProvider // adjust the target from implmentation to signature | Signature, Implementation -> return! gotoDefinitionService.NavigateToSymbolDeclarationAsync (targetDoc, targetSource, range)|>liftAsync - } |> Async.map (Option.map (fun res -> - if res then - SessionHandling.currentSession - |> Option.iter (fun session -> session.Dismiss ()) - )) |> Async.Ignore |> Async.StartImmediate + } + |> Async.map( + function + | Some true -> SessionHandling.currentSession |> Option.iter (fun session -> session.Dismiss()) + | _ -> ()) + |> Async.Ignore + |> Async.StartImmediate let formatMap = typemap.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" let layoutTagToFormatting (layoutTag: LayoutTag) = layoutTag - |> roslynTag + |> RoslynHelpers.roslynTag |> ClassificationTags.GetClassificationTypeName |> typemap.GetClassificationType |> formatMap.GetTextProperties - let inlines = seq { - for taggedText in content do - let run = Documents.Run taggedText.Text - let inl = - match taggedText with - | :? Layout.NavigableTaggedText as nav when thisSymbolUseRange <> nav.Range && isTargetValid nav.Range -> - let h = SourceLink (run, ToolTip = nav.Range.FileName) - h.Click.Add <| fun _ -> navigateTo nav.Range - h :> Documents.Inline - | _ -> run :> _ - DependencyObjectExtensions.SetTextProperties (inl, layoutTagToFormatting taggedText.Tag) - yield inl - } + let inlines = + seq { + for taggedText in content do + let run = Documents.Run taggedText.Text + let inl = + match taggedText with + | :? Layout.NavigableTaggedText as nav when thisSymbolUseRange <> nav.Range && isTargetValid nav.Range -> + let h = SourceLink (run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + DependencyObjectExtensions.SetTextProperties (inl, layoutTagToFormatting taggedText.Tag) + yield inl + } let createTextLinks () = let tb = TextBlock(TextWrapping = TextWrapping.Wrap, TextTrimming = TextTrimming.None) @@ -275,11 +294,11 @@ type internal FSharpQuickInfoProvider if tb.Inlines.Count = 0 then tb.Visibility <- Visibility.Collapsed tb :> FrameworkElement - { new IDeferredQuickInfoContent with member x.Create () = createTextLinks () } + { new IDeferredQuickInfoContent with member x.Create() = createTextLinks() } let xmlMemberIndexService = serviceProvider.GetService(typeof) :?> IVsXMLMemberIndexService let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService, serviceProvider.DTE) - + static member ProvideQuickInfo(checker: FSharpChecker, documentId: DocumentId, sourceText: SourceText, filePath: string, position: int, options: FSharpProjectOptions, textVersionHash: int) = asyncMaybe { let! _, _, checkFileResults = checker.ParseAndCheckDocument (filePath, textVersionHash, sourceText.ToString(), options, allowStaleResults = true) @@ -293,33 +312,33 @@ type internal FSharpQuickInfoProvider | FSharpToolTipText [FSharpStructuredToolTipElement.None] -> return! None | _ -> let! symbolUse = checkFileResults.GetSymbolUseAtLocation (textLineNumber, symbol.Ident.idRange.EndColumn, textLine.ToString(), symbol.FullIsland) - return! Some (res, RoslynHelpers.FSharpRangeToTextSpan (sourceText, symbol.Range), symbolUse.Symbol, symbol.Kind) + return res, RoslynHelpers.FSharpRangeToTextSpan (sourceText, symbol.Range), symbolUse.Symbol, symbol.Kind } interface IQuickInfoProvider with override this.GetItemAsync(document: Document, position: int, cancellationToken: CancellationToken): Task = asyncMaybe { - let! sigTooltipInfo, targetTooltipInfo = - FSharpQuickInfo.getCompoundTooltipInfo(checkerProvider.Checker, position, document, projectInfoManager, cancellationToken) + let! symbolUse, sigTooltipInfo, targetTooltipInfo = + FSharpQuickInfo.getTooltipInfo(checkerProvider.Checker, projectInfoManager, document, position, cancellationToken) match sigTooltipInfo, targetTooltipInfo with | None, None -> return null - | Some (toolTipElement, textSpan, symbolUse, symbolKind), None - | None, Some (toolTipElement, textSpan, symbolUse, symbolKind) -> - let mainDescription = Collections.Generic.List () - let documentation = Collections.Generic.List () - XmlDocumentation.BuildDataTipText(documentationBuilder, mainDescription.Add, documentation.Add, toolTipElement) + | Some tooltip, None + | None, Some tooltip -> + let mainDescription = ResizeArray() + let documentation = ResizeArray() + XmlDocumentation.BuildDataTipText(documentationBuilder, mainDescription.Add, documentation.Add, tooltip.StructuredText) let content = - FSharpQuickInfo.tooltip - (SymbolGlyphDeferredContent(GetGlyphForSymbol(symbolUse.Symbol, symbolKind), glyphService), + FSharpQuickInfo.createDeferredContent + (SymbolGlyphDeferredContent(Tokenizer.GetGlyphForSymbol(tooltip.Symbol, tooltip.SymbolKind), glyphService), fragment (mainDescription, typeMap, document, symbolUse.RangeAlternate), fragment (documentation, typeMap, document, symbolUse.RangeAlternate)) - return QuickInfoItem (textSpan, content) + return QuickInfoItem (tooltip.Span, content) - | Some (sigToolTipElement, _, _, _), Some (targetToolTipElement, targetTextSpan, targetSymbolUse, targetSymbolKind) -> + | Some sigTooltip, Some targetTooltip -> let description, targetDocumentation, sigDocumentation = ResizeArray(), ResizeArray(), ResizeArray() - XmlDocumentation.BuildDataTipText(documentationBuilder, ignore, sigDocumentation.Add, sigToolTipElement) - XmlDocumentation.BuildDataTipText(documentationBuilder, description.Add, targetDocumentation.Add, targetToolTipElement) + XmlDocumentation.BuildDataTipText(documentationBuilder, ignore, sigDocumentation.Add, sigTooltip.StructuredText) + XmlDocumentation.BuildDataTipText(documentationBuilder, description.Add, targetDocumentation.Add, targetTooltip.StructuredText) let width = description @@ -333,19 +352,21 @@ type internal FSharpQuickInfoProvider // get whitespace nomalized documentation text let getText (tts: seq) = - ((StringBuilder(), tts) ||> Seq.fold (fun sb tt -> - if String.IsNullOrWhiteSpace tt.Text then sb else sb.Append tt.Text)).ToString() + let text = + (StringBuilder(), tts) + ||> Seq.fold (fun sb tt -> + if String.IsNullOrWhiteSpace tt.Text then sb else sb.Append tt.Text) + |> string + if String.IsNullOrWhiteSpace text then None else Some text let documentation = - let implText, sigText = getText targetDocumentation, getText sigDocumentation - let implDocsEmpty, sigDocsEmpty = String.IsNullOrWhiteSpace implText, String.IsNullOrWhiteSpace sigText - - [ match implDocsEmpty, sigDocsEmpty with - | true, true -> () - | true, false -> yield! sigDocumentation - | false, true -> yield! targetDocumentation - | false, false when implText.Equals (sigText, StringComparison.OrdinalIgnoreCase) -> yield! sigDocumentation - | false, false -> + [ match getText targetDocumentation, getText sigDocumentation with + | None, None -> () + | None, Some _ -> yield! sigDocumentation + | Some _, None -> yield! targetDocumentation + | Some implText, Some sigText when implText.Equals (sigText, StringComparison.OrdinalIgnoreCase) -> + yield! sigDocumentation + | Some _, Some _ -> yield! sigDocumentation yield lineBreak yield seperator @@ -353,11 +374,11 @@ type internal FSharpQuickInfoProvider yield! targetDocumentation ] let content = - FSharpQuickInfo.tooltip - (SymbolGlyphDeferredContent (GetGlyphForSymbol (targetSymbolUse.Symbol, targetSymbolKind), glyphService), - fragment (description, typeMap, document, targetSymbolUse.RangeAlternate), - fragment (documentation, typeMap, document, targetSymbolUse.RangeAlternate)) + FSharpQuickInfo.createDeferredContent + (SymbolGlyphDeferredContent (Tokenizer.GetGlyphForSymbol (targetTooltip.Symbol, targetTooltip.SymbolKind), glyphService), + fragment (description, typeMap, document, symbolUse.RangeAlternate), + fragment (documentation, typeMap, document, symbolUse.RangeAlternate)) - return QuickInfoItem (targetTextSpan, content) + return QuickInfoItem (targetTooltip.Span, content) } |> Async.map Option.toObj |> RoslynHelpers.StartAsyncAsTask cancellationToken diff --git a/vsintegration/tests/unittests/ColorizationServiceTests.fs b/vsintegration/tests/unittests/ColorizationServiceTests.fs index 2a016069b..dab38daad 100644 --- a/vsintegration/tests/unittests/ColorizationServiceTests.fs +++ b/vsintegration/tests/unittests/ColorizationServiceTests.fs @@ -10,7 +10,6 @@ open Microsoft.CodeAnalysis.Classification open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor -open RoslynHelpers [][] type ColorizationServiceTests() = @@ -36,7 +35,6 @@ type ColorizationServiceTests() = | None -> Assert.Fail("Cannot find colorization data for end of marker") | Some(classifiedSpan) -> Assert.AreEqual(classificationType, classifiedSpan.ClassificationType, "Classification data doesn't match for end of marker") - [] member this.Comment_SingleLine() = this.VerifyColorizerAtEndOfMarker( -- GitLab