diff --git a/src/fsharp/TypeChecker.fs b/src/fsharp/TypeChecker.fs index d02c44e78b0..e59a91b151f 100755 --- a/src/fsharp/TypeChecker.fs +++ b/src/fsharp/TypeChecker.fs @@ -3911,7 +3911,7 @@ type DelayedItem = /// DelayedApp (isAtomic, argExpr, mFuncAndArg) /// - /// Represents the args in "item args", or "item.[args]". + /// Represents the args in "item args", or "item.[args]". | DelayedApp of ExprAtomicFlag * Ast.SynExpr * range /// Represents the long identifiers in "item.Ident1", or "item.Ident1.Ident2" etc. @@ -5328,7 +5328,15 @@ and RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects_Delayed cenv e RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects cenv env tpenv arg dummyCheckedDelayed otherDelayed | _ -> () - dummyCheckedDelayed delayed + dummyCheckedDelayed delayed + +// Calls UnifyTypes, but upon error only does the minimal error recovery +// so that IntelliSense information can continue to be collected. +and UnifyTypesAndRecover cenv env m expectedTy actualTy = + try + UnifyTypes cenv env m expectedTy actualTy + with e -> + errorRecovery e m and TcExprOfUnknownType cenv env tpenv expr = let exprty = NewInferenceType () @@ -8094,7 +8102,7 @@ and PropagateThenTcDelayed cenv overallTy env tpenv mExpr expr exprty (atomicFla | [] -> // Avoid unifying twice: we're about to unify in TcDelayed if not (isNil delayed) then - UnifyTypes cenv env mExpr overallTy exprty + UnifyTypesAndRecover cenv env mExpr overallTy exprty | DelayedDot :: _ | DelayedSet _ :: _ | DelayedDotLookup _ :: _ -> () @@ -8113,7 +8121,7 @@ and PropagateThenTcDelayed cenv overallTy env tpenv mExpr expr exprty (atomicFla | SynExpr.CompExpr _ -> () | _ -> // 'delayed' is about to be dropped on the floor, first do rudimentary checking to get name resolutions in its body - RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects_Delayed cenv env tpenv delayed + RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects_Delayed cenv env tpenv delayed error (NotAFunction(denv,overallTy,mExpr,mArg)) propagate delayed expr.Range exprty @@ -8132,7 +8140,8 @@ and TcDelayed cenv overallTy env tpenv mExpr expr exprty (atomicFlag:ExprAtomicF match delayed with | [] | DelayedDot :: _ -> - UnifyTypes cenv env mExpr overallTy exprty; expr.Expr,tpenv + UnifyTypesAndRecover cenv env mExpr overallTy exprty + expr.Expr,tpenv // expr.M(args) where x.M is a .NET method or index property // expr.M(args) where x.M is a .NET method or index property // expr.M where x.M is a .NET method or index property @@ -8694,7 +8703,7 @@ and TcItemThen cenv overallTy env tpenv (item,mItem,rest,afterOverloadResolution | Item.CustomOperation (nm,usageTextOpt,_) -> // 'delayed' is about to be dropped on the floor, first do rudimentary checking to get name resolutions in its body - RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects_Delayed cenv env tpenv delayed + RecordNameAndTypeResolutions_IdeallyWithoutHavingOtherEffects_Delayed cenv env tpenv delayed match usageTextOpt() with | None -> error(Error(FSComp.SR.tcCustomOperationNotUsedCorrectly(nm), mItem)) | Some usageText -> error(Error(FSComp.SR.tcCustomOperationNotUsedCorrectly2(nm,usageText), mItem)) diff --git a/src/fsharp/vs/service.fs b/src/fsharp/vs/service.fs index bfb5b012716..e31c120fc73 100755 --- a/src/fsharp/vs/service.fs +++ b/src/fsharp/vs/service.fs @@ -899,7 +899,7 @@ type TypeCheckInfo | None, _ -> [], None | Some(origLongIdent), Some _ -> origLongIdent, None | Some(origLongIdent), None -> - assert (not (isNil origLongIdent)) + System.Diagnostics.Debug.Assert(not (isNil origLongIdent), "origLongIdent is empty") // note: as above, this happens when we are called for "precise" resolution - (F1 keyword, data tip etc..) let plid, residue = List.frontAndBack origLongIdent plid, Some residue @@ -1370,20 +1370,30 @@ type TypeCheckInfo // Not, this does not have to be a SyncOp, it can be called from any thread member scope.GetExtraColorizations() = - [| for cnr in sResolutions.CapturedNameResolutions do - match cnr with - // 'seq' in 'seq { ... }' gets colored as keywords - | CNR(_, (Item.Value vref), ItemOccurence.Use, _, _, _, m) when valRefEq g g.seq_vref vref -> - yield (m, FSharpTokenColorKind.Keyword) - // custom builders, custom operations get colored as keywords - | CNR(_, (Item.CustomBuilder _ | Item.CustomOperation _), ItemOccurence.Use, _, _, _, m) -> - yield (m, FSharpTokenColorKind.Keyword) - // types get colored as types when they occur in syntactic types or custom attributes - // typevariables get colored as types when they occur in syntactic types custom builders, custom operations get colored as keywords - | CNR(_, (Item.TypeVar _ | Item.Types _ | Item.UnqualifiedType _) , (ItemOccurence.UseInType | ItemOccurence.UseInAttribute), _, _, _, m) -> - yield (m, FSharpTokenColorKind.TypeName) - | _ -> () - |] + sResolutions.CapturedNameResolutions + |> Seq.choose (fun cnr -> + match cnr with + // 'seq' in 'seq { ... }' gets colored as keywords + | CNR(_, (Item.Value vref), ItemOccurence.Use, _, _, _, m) when valRefEq g g.seq_vref vref -> + Some (m, FSharpTokenColorKind.Keyword) + // custom builders, custom operations get colored as keywords + | CNR(_, (Item.CustomBuilder _ | Item.CustomOperation _), ItemOccurence.Use, _, _, _, m) -> + Some (m, FSharpTokenColorKind.Keyword) + // types get colored as types when they occur in syntactic types or custom attributes + // typevariables get colored as types when they occur in syntactic types custom builders, custom operations get colored as keywords + | CNR(_, ( Item.TypeVar _ + | Item.Types _ + | Item.UnqualifiedType _ + | Item.CtorGroup _), + ( ItemOccurence.UseInType + | ItemOccurence.UseInAttribute + | ItemOccurence.Use _ + | ItemOccurence.Binding _ + | ItemOccurence.Pattern _), _, _, _, m) -> + Some (m, FSharpTokenColorKind.TypeName) + | _ -> None) + |> Seq.toArray + member x.ScopeResolutions = sResolutions member x.ScopeSymbolUses = sSymbolUses member x.TcGlobals = g diff --git a/tests/fsharp/typeProviders/negTests/neg1.bsl b/tests/fsharp/typeProviders/negTests/neg1.bsl index 64fd3e58aef..668eec00e10 100644 --- a/tests/fsharp/typeProviders/negTests/neg1.bsl +++ b/tests/fsharp/typeProviders/negTests/neg1.bsl @@ -1966,16 +1966,22 @@ neg1.fsx(438,109,438,110): typecheck error FS0001: This expression was expected but here has type 'string' +neg1.fsx(438,9,438,111): typecheck error FS3033: The type provider 'Provider.GoodProviderForNegativeStaticParameterTypeTests' reported an error: Specified cast is not valid. + neg1.fsx(440,119,440,120): typecheck error FS0001: This expression was expected to have type 'int' but here has type 'string' +neg1.fsx(440,19,440,121): typecheck error FS3033: The type provider 'Provider.GoodProviderForNegativeStaticParameterTypeTests' reported an error: Specified cast is not valid. + neg1.fsx(440,119,440,120): typecheck error FS0001: This expression was expected to have type 'int' but here has type 'string' +neg1.fsx(440,19,440,121): typecheck error FS3033: The type provider 'Provider.GoodProviderForNegativeStaticParameterTypeTests' reported an error: Specified cast is not valid. + neg1.fsx(448,9,448,107): typecheck error FS3148: Too many static parameters. Expected at most 1 parameters, but got 2 unnamed and 0 named parameters. neg1.fsx(449,105,449,110): typecheck error FS3083: The static parameter 'Count' has already been given a value diff --git a/tests/fsharp/typecheck/sigs/neg04.bsl b/tests/fsharp/typecheck/sigs/neg04.bsl index e32b4fb0154..72a8ee16dd6 100644 --- a/tests/fsharp/typecheck/sigs/neg04.bsl +++ b/tests/fsharp/typecheck/sigs/neg04.bsl @@ -31,6 +31,14 @@ but given a ''g list -> 'h' The type 'seq<'a>' does not match the type ''f list' +neg04.fs(47,49,47,51): typecheck error FS0784: This numeric literal requires that a module 'NumericLiteralN' defining functions FromZero, FromOne, FromInt32, FromInt64 and FromString be in scope + +neg04.fs(47,30,47,51): typecheck error FS0001: Type mismatch. Expecting a + 'seq<'a> -> 'f' +but given a + ''g list -> 'h' +The type 'seq<'a>' does not match the type ''f list' + neg04.fs(61,25,61,40): typecheck error FS0001: This expression was expected to have type 'ClassType1' but here has type diff --git a/tests/fsharp/typecheck/sigs/neg20.bsl b/tests/fsharp/typecheck/sigs/neg20.bsl index 8c593a51bf4..1ac552867f3 100644 --- a/tests/fsharp/typecheck/sigs/neg20.bsl +++ b/tests/fsharp/typecheck/sigs/neg20.bsl @@ -245,6 +245,16 @@ neg20.fs(184,28,184,33): typecheck error FS0001: This expression was expected to but here has type 'obj' +neg20.fs(184,28,184,33): typecheck error FS0001: This expression was expected to have type + 'int' +but here has type + 'obj' + +neg20.fs(184,34,184,39): typecheck error FS0001: This expression was expected to have type + 'int' +but here has type + 'obj' + neg20.fs(184,34,184,39): typecheck error FS0001: This expression was expected to have type 'int' but here has type @@ -272,6 +282,16 @@ neg20.fs(190,28,190,33): typecheck error FS0001: This expression was expected to but here has type 'obj' +neg20.fs(190,28,190,33): typecheck error FS0001: This expression was expected to have type + 'string' +but here has type + 'obj' + +neg20.fs(190,34,190,39): typecheck error FS0001: This expression was expected to have type + 'string' +but here has type + 'obj' + neg20.fs(190,34,190,39): typecheck error FS0001: This expression was expected to have type 'string' but here has type diff --git a/tests/fsharp/typecheck/sigs/neg22.bsl b/tests/fsharp/typecheck/sigs/neg22.bsl index 502aa7cc8bd..a408371e221 100644 --- a/tests/fsharp/typecheck/sigs/neg22.bsl +++ b/tests/fsharp/typecheck/sigs/neg22.bsl @@ -21,6 +21,18 @@ but given a 'float' The unit of measure 'm' does not match the unit of measure 'kg' +neg22.fs(17,22,17,29): typecheck error FS0001: Type mismatch. Expecting a + 'float' +but given a + 'float' +The unit of measure 'm' does not match the unit of measure 'kg' + +neg22.fs(17,20,17,29): typecheck error FS0001: Type mismatch. Expecting a + 'float' +but given a + 'float' +The unit of measure 'm' does not match the unit of measure 'kg' + neg22.fs(28,12,28,18): typecheck error FS0957: The declared type parameters for this type extension do not match the declared type parameters on the original type 'LibGen<_>' neg22.fs(40,12,40,18): typecheck error FS0957: The declared type parameters for this type extension do not match the declared type parameters on the original type 'LibGen<_>' diff --git a/tests/fsharpqa/Source/Conformance/TypesAndTypeConstraints/CheckingSyntacticTypes/E_Regression02.fs b/tests/fsharpqa/Source/Conformance/TypesAndTypeConstraints/CheckingSyntacticTypes/E_Regression02.fs index 9e21caf6bfa..deb1f392352 100644 --- a/tests/fsharpqa/Source/Conformance/TypesAndTypeConstraints/CheckingSyntacticTypes/E_Regression02.fs +++ b/tests/fsharpqa/Source/Conformance/TypesAndTypeConstraints/CheckingSyntacticTypes/E_Regression02.fs @@ -1,70 +1,106 @@ // #Regression #Conformance #TypeConstraints // Regression test for CTP bug reported at http://cs.hubfs.net/forums/thread/9313.aspx // In CTP, this was a stack overflow in the compiler. Now we give 64 errors -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' -//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P1' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined //The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P2' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P3' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P4' is not defined +//The type 'uint32' does not match the type 'int32' +//The value or constructor 'P4' is not defined module SmartHashUtils = let ByteToUInt (array:byte[]) offset length endian = diff --git a/vsintegration/src/FSharp.Editor/ColorizationService.fs b/vsintegration/src/FSharp.Editor/ColorizationService.fs index 6f7e357dc4d..9060eb0b249 100644 --- a/vsintegration/src/FSharp.Editor/ColorizationService.fs +++ b/vsintegration/src/FSharp.Editor/ColorizationService.fs @@ -28,169 +28,22 @@ open Microsoft.VisualStudio.Utilities open Microsoft.FSharp.Compiler.SourceCodeServices -type private SourceLineData(lineStart: int, lexStateAtStartOfLine: FSharpTokenizerLexState, lexStateAtEndOfLine: FSharpTokenizerLexState, hashCode: int, classifiedSpans: IReadOnlyList) = - member val LineStart = lineStart - member val LexStateAtStartOfLine = lexStateAtStartOfLine - member val LexStateAtEndOfLine = lexStateAtEndOfLine - member val HashCode = hashCode - member val ClassifiedSpans = classifiedSpans - - 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 - - member x.ClearFrom(n) = - let mutable i = n - while i < data.Count && data.[i].IsSome do - data.[i] <- None - i <- i + 1 - - - [, FSharpCommonConstants.FSharpLanguageName)>] type internal FSharpColorizationService() = - - static let DataCache = ConditionalWeakTable() - static let Tokenizers = ConditionalWeakTable<(string list * string option), FSharpSourceTokenizer>() - - static 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.TypeName -> ClassificationTypeNames.ClassName - | FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text - - static let 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 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 - tokenInfoOption - - 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() - - 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) - - static member GetColorizationData(documentKey: DocumentId, sourceText: SourceText, textSpan: TextSpan, fileName: Option, defines: string list, cancellationToken: CancellationToken) : List = - try - let sourceTokenizer = Tokenizers.GetValue ((defines, fileName), fun _ -> 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-1] 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 = 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) - - 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 - - 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 ex -> - Assert.Exception(ex) - List() - interface IEditorClassificationService with - // Do not perform classification if we don't have project options (#defines matter) member this.AddLexicalClassifications(_: SourceText, _: TextSpan, _: List, _: CancellationToken) = () member this.AddSyntacticClassificationsAsync(document: Document, textSpan: TextSpan, result: List, cancellationToken: CancellationToken) = - document.GetTextAsync(cancellationToken).ContinueWith( - fun (sourceTextTask: Task) -> - match FSharpLanguageService.GetOptions(document.Project.Id) with - | Some(options) -> - let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) - if sourceTextTask.Status = TaskStatus.RanToCompletion then - result.AddRange(FSharpColorizationService.GetColorizationData(document.Id, sourceTextTask.Result, textSpan, Some(document.FilePath), defines, cancellationToken)) - | None -> () - , cancellationToken) + async { + let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask + match FSharpLanguageService.GetOptions(document.Project.Id) with + | Some(options) -> + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) + result.AddRange(CommonHelpers.getColorizationData( + document.Id, sourceText, textSpan, Some(document.FilePath), defines, cancellationToken)) + | None -> () + } |> CommonRoslynHelpers.StartAsyncUnitAsTask(cancellationToken) member this.AddSemanticClassificationsAsync(document: Document, textSpan: TextSpan, result: List, cancellationToken: CancellationToken) = async { @@ -204,7 +57,10 @@ type internal FSharpColorizationService() = match checkResultsAnswer with | FSharpCheckFileAnswer.Aborted -> [| |] | FSharpCheckFileAnswer.Succeeded(results) -> results.GetExtraColorizationsAlternate() - |> Array.map(fun (range, tokenColorKind) -> ClassifiedSpan(CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, range), compilerTokenToRoslynToken(tokenColorKind))) + |> Seq.map(fun (range, tokenColorKind) -> + ClassifiedSpan(CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, range), + CommonHelpers.compilerTokenToRoslynToken(tokenColorKind))) + |> Seq.toList result.AddRange(extraColorizationData) | None -> () diff --git a/vsintegration/src/FSharp.Editor/CommonHelpers.fs b/vsintegration/src/FSharp.Editor/CommonHelpers.fs new file mode 100644 index 00000000000..c177ff13e46 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CommonHelpers.fs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Classification +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.LanguageService +open Microsoft.FSharp.Compiler.SourceCodeServices + +module CommonHelpers = + type private SourceLineData(lineStart: int, lexStateAtStartOfLine: FSharpTokenizerLexState, lexStateAtEndOfLine: FSharpTokenizerLexState, hashCode: int, classifiedSpans: IReadOnlyList) = + member val LineStart = lineStart + member val LexStateAtStartOfLine = lexStateAtStartOfLine + member val LexStateAtEndOfLine = lexStateAtEndOfLine + member val HashCode = hashCode + member val ClassifiedSpans = classifiedSpans + + 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 + + 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 tokenizers = ConditionalWeakTable<(string list * string option), FSharpSourceTokenizer>() + + let internal 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.TypeName -> ClassificationTypeNames.ClassName + | 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 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 + tokenInfoOption + + 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() + + 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) + + let getColorizationData(documentKey: DocumentId, sourceText: SourceText, textSpan: TextSpan, fileName: string option, defines: string list, cancellationToken: CancellationToken) : List = + try + let sourceTokenizer = tokenizers.GetValue ((defines, fileName), fun _ -> 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-1] 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) + + 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 + + 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 ex -> + Assert.Exception(ex) + List() + + let tryClassifyAtPosition (documentKey, sourceText: SourceText, filePath, defines, position: int, cancellationToken) = + let textLine = sourceText.Lines.GetLineFromPosition(position) + let textLinePos = sourceText.Lines.GetLinePosition(position) + let textLineColumn = textLinePos.Character + + let classifiedSpanOption = + getColorizationData(documentKey, sourceText, textLine.Span, Some(filePath), defines, cancellationToken) + |> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(position)) + + match classifiedSpanOption with + | Some(classifiedSpan) -> + match classifiedSpan.ClassificationType with + | ClassificationTypeNames.ClassName + | ClassificationTypeNames.DelegateName + | ClassificationTypeNames.EnumName + | ClassificationTypeNames.InterfaceName + | ClassificationTypeNames.ModuleName + | ClassificationTypeNames.StructName + | ClassificationTypeNames.TypeParameterName + | ClassificationTypeNames.Identifier -> + match QuickParse.GetCompleteIdentifierIsland true (textLine.ToString()) textLineColumn with + | Some (islandIdentifier, islandColumn, isQuoted) -> + let qualifiers = if isQuoted then [islandIdentifier] else islandIdentifier.Split '.' |> Array.toList + Some (islandColumn, qualifiers, classifiedSpan.TextSpan) + | None -> None + | ClassificationTypeNames.Operator -> + let islandColumn = sourceText.Lines.GetLinePositionSpan(classifiedSpan.TextSpan).End.Character + Some (islandColumn, [""], classifiedSpan.TextSpan) + | _ -> None + | _ -> None + +open System.Reflection +open Microsoft.VisualStudio.Shell +open System.Windows.Controls +// A port of https://github.com/tomasr/viasfora/blob/master/Viasfora/Util/VsColors.cs +type VSColors() = + static let colorTypeOpt = + try + let vsShellAssembly = Assembly.Load("Microsoft.VisualStudio.Shell.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") + vsShellAssembly + |> Option.ofObj + |> Option.map (fun assembly -> + assembly.GetType("Microsoft.VisualStudio.PlatformUI.EnvironmentColors")) + with _ -> + None + + static let tryGetOrElse (key: string) (alternate: obj) = + colorTypeOpt + |> Option.map (fun colorType -> + let prop = colorType.GetProperty(key) + prop.GetValue(null, null)) + |> function Some x -> x | None -> alternate + + static member CommandShelfBackgroundGradientBrushKey = tryGetOrElse "CommandShelfBackgroundGradientBrushKey" VsBrushes.CommandBarGradientBeginKey + static member CommandBarTextActiveBrushKey = tryGetOrElse "CommandBarTextActiveBrushKey" VsBrushes.CommandBarTextActiveKey + static member ToolTipBrushKey = tryGetOrElse "ToolTipBrushKey" TextBlock.BackgroundProperty + static member ToolTipTextBrushKey = tryGetOrElse "ToolTipTextBrushKey" TextBlock.ForegroundProperty \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/CompletionProvider.fs index 1fbceb30c1a..819eec560b1 100644 --- a/vsintegration/src/FSharp.Editor/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/CompletionProvider.fs @@ -66,7 +66,7 @@ type internal FSharpCompletionProvider(workspace: Workspace, serviceProvider: SV let textLines = sourceText.Lines let triggerLine = textLines.GetLineFromPosition(triggerPosition) let classifiedSpanOption = - FSharpColorizationService.GetColorizationData(documentId, sourceText, triggerLine.Span, Some(filePath), defines, CancellationToken.None) + CommonHelpers.getColorizationData(documentId, sourceText, triggerLine.Span, Some(filePath), defines, CancellationToken.None) |> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(triggerPosition)) match classifiedSpanOption with diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index a7b2fd25bf8..b4b3cb163aa 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -31,6 +31,7 @@ + Classification\ColorizationService.fs @@ -65,6 +66,9 @@ BlockComment\CommentUncommentService.fs + + QuickInfo\QuickInfoProvider.fs + @@ -90,6 +94,8 @@ + + @@ -107,6 +113,9 @@ $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Text.UI.Wpf.$(RoslynVSPackagesVersion)\lib\net45\Microsoft.VisualStudio.Text.UI.Wpf.dll + + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Language.Intellisense.$(RoslynVSPackagesVersion)\lib\net45\Microsoft.VisualStudio.Language.Intellisense.dll + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Text.Data.$(RoslynVSPackagesVersion)\lib\net45\Microsoft.VisualStudio.Text.Data.dll diff --git a/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs index f8ae37c4052..3b03624316c 100644 --- a/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs @@ -4,7 +4,6 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Composition -open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable open System.Linq @@ -43,68 +42,33 @@ type internal FSharpNavigableItem(document: Document, textSpan: TextSpan) = [, FSharpCommonConstants.FSharpLanguageName)>] type internal FSharpGoToDefinitionService [] ([]presenters: IEnumerable) = - static member FindDefinition (documentKey: DocumentId, - sourceText: SourceText, - filePath: string, - position: int, - defines: string list, - options: FSharpProjectOptions, - textVersionHash: int, - cancellationToken: CancellationToken) - : Async> = - async { - - let textLine = sourceText.Lines.GetLineFromPosition(position) - let textLinePos = sourceText.Lines.GetLinePosition(position) - let fcsTextLineNumber = Line.fromZ textLinePos.Line // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based - let textLineColumn = textLinePos.Character - let tryClassifyAtPosition (position: int) = - let classifiedSpanOption = - FSharpColorizationService.GetColorizationData(documentKey, sourceText, textLine.Span, Some(filePath), defines, cancellationToken) - |> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(position)) - - match classifiedSpanOption with - | Some(classifiedSpan) -> - match classifiedSpan.ClassificationType with - | ClassificationTypeNames.ClassName - | ClassificationTypeNames.DelegateName - | ClassificationTypeNames.EnumName - | ClassificationTypeNames.InterfaceName - | ClassificationTypeNames.ModuleName - | ClassificationTypeNames.StructName - | ClassificationTypeNames.TypeParameterName - | ClassificationTypeNames.Identifier -> - match QuickParse.GetCompleteIdentifierIsland true (textLine.ToString()) textLineColumn with - | Some (islandIdentifier, islandColumn, isQuoted) -> - let qualifiers = if isQuoted then [islandIdentifier] else islandIdentifier.Split '.' |> Array.toList - Some (islandColumn, qualifiers) - | None -> None - | ClassificationTypeNames.Operator -> - let islandColumn = sourceText.Lines.GetLinePositionSpan(classifiedSpan.TextSpan).End.Character - Some (islandColumn, [""]) - | _ -> None - | _ -> None - - // Tolerate being on the right of the identifier - let quickParseInfo = - match tryClassifyAtPosition position with - | None when textLineColumn > 0 -> tryClassifyAtPosition (position - 1) - | res -> res - - match quickParseInfo with - | Some (islandColumn, qualifiers) -> - let! _parseResults, checkFileAnswer = FSharpLanguageService.Checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToString(), options) - match checkFileAnswer with - | FSharpCheckFileAnswer.Aborted -> return None - | FSharpCheckFileAnswer.Succeeded(checkFileResults) -> - - let! declarations = checkFileResults.GetDeclarationLocationAlternate (fcsTextLineNumber, islandColumn, textLine.ToString(), qualifiers, false) - - match declarations with - | FSharpFindDeclResult.DeclFound(range) -> return Some(range) - | _ -> return None - | None -> return None - } + static member FindDefinition(documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, defines: string list, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) : Async> = + async { + let textLine = sourceText.Lines.GetLineFromPosition(position) + let textLinePos = sourceText.Lines.GetLinePosition(position) + let fcsTextLineNumber = textLinePos.Line + 1 // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based + let textLineColumn = textLinePos.Character + let tryClassifyAtPosition position = CommonHelpers.tryClassifyAtPosition(documentKey, sourceText, filePath, defines, position, cancellationToken) + // Tolerate being on the right of the identifier + let quickParseInfo = + match tryClassifyAtPosition position with + | None when textLineColumn > 0 -> tryClassifyAtPosition (position - 1) + | res -> res + + match quickParseInfo with + | Some (islandColumn, qualifiers, _) -> + let! _parseResults, checkFileAnswer = FSharpLanguageService.Checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToString(), options) + match checkFileAnswer with + | FSharpCheckFileAnswer.Aborted -> return None + | FSharpCheckFileAnswer.Succeeded(checkFileResults) -> + + let! declarations = checkFileResults.GetDeclarationLocationAlternate (fcsTextLineNumber, islandColumn, textLine.ToString(), qualifiers, false) + + match declarations with + | FSharpFindDeclResult.DeclFound(range) -> return Some(range) + | _ -> return None + | None -> return None + } // FSROSLYNTODO: Since we are not integrated with the Roslyn project system yet, the below call // document.Project.Solution.GetDocumentIdsWithFilePath() will only access files in the same project. diff --git a/vsintegration/src/FSharp.Editor/LanguageDebugInfoService.fs b/vsintegration/src/FSharp.Editor/LanguageDebugInfoService.fs index d7808258112..dab7714df06 100644 --- a/vsintegration/src/FSharp.Editor/LanguageDebugInfoService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageDebugInfoService.fs @@ -71,7 +71,7 @@ type internal FSharpLanguageDebugInfoService() = let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask let textSpan = TextSpan.FromBounds(0, sourceText.Length) - let tokens = FSharpColorizationService.GetColorizationData(document.Id, sourceText, textSpan, Some(document.Name), defines, cancellationToken) + let tokens = CommonHelpers.getColorizationData(document.Id, sourceText, textSpan, Some(document.Name), defines, cancellationToken) return match FSharpLanguageDebugInfoService.GetDataTipInformation(sourceText, position, tokens) with | None -> Unchecked.defaultof | Some(textSpan) -> new DebugDataTipInfo(textSpan, sourceText.GetSubText(textSpan).ToString()) diff --git a/vsintegration/src/FSharp.Editor/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfoProvider.fs new file mode 100644 index 00000000000..6f061d08432 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/QuickInfoProvider.fs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Collections.Concurrent +open System.Collections.Generic +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks +open System.Linq +open System.Runtime.CompilerServices +open System.Windows +open System.Windows.Controls +open System.Windows.Media + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Completion +open Microsoft.CodeAnalysis.Classification +open Microsoft.CodeAnalysis.Editor +open Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +open Microsoft.CodeAnalysis.Editor.Shared.Utilities +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.CodeAnalysis.Options +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.LanguageService +open Microsoft.VisualStudio.Text +open Microsoft.VisualStudio.Text.Tagging +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop + +open Microsoft.FSharp.Compiler +open Microsoft.FSharp.Compiler.Parser +open Microsoft.FSharp.Compiler.Range +open Microsoft.FSharp.Compiler.SourceCodeServices + +// FSROSLYNTODO: with the merge of the below PR, the QuickInfo API should be changed +// to allow for a more flexible syntax for defining the content of the tooltip. +// The below interface should be discarded then or updated accourdingly. +// https://github.com/dotnet/roslyn/pull/13623 +type internal FSharpDeferredQuickInfoContent(content: string) = + interface IDeferredQuickInfoContent with + override this.Create() : FrameworkElement = + let label = Label(Content = content, Foreground = SolidColorBrush(Colors.Black)) + label.SetResourceReference(TextBlock.BackgroundProperty, VSColors.ToolTipBrushKey) + label.SetResourceReference(TextBlock.ForegroundProperty, VSColors.ToolTipTextBrushKey) + upcast label + +[] +[] +type internal FSharpQuickInfoProvider [] + ([)>] serviceProvider: IServiceProvider) = + + let xmlMemberIndexService = serviceProvider.GetService(typeof) :?> IVsXMLMemberIndexService + let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService, serviceProvider.DTE) + + static member ProvideQuickInfo(documentId: DocumentId, sourceText: SourceText, filePath: string, position: int, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) = + async { + let! _parseResults, checkResultsAnswer = FSharpChecker.Instance.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToString(), options) + let checkFileResults = + match checkResultsAnswer with + | FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet" + | FSharpCheckFileAnswer.Succeeded(results) -> results + + let textLine = sourceText.Lines.GetLineFromPosition(position) + let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based + let textLinePos = sourceText.Lines.GetLinePosition(position) + let textLineColumn = textLinePos.Character + //let qualifyingNames, partialName = QuickParse.GetPartialLongNameEx(textLine.ToString(), textLineColumn - 1) + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, options.OtherOptions |> Seq.toList) + let tryClassifyAtPosition position = + CommonHelpers.tryClassifyAtPosition(documentId, sourceText, filePath, defines, position, cancellationToken) + + let quickParseInfo = + match tryClassifyAtPosition position with + | None when textLineColumn > 0 -> tryClassifyAtPosition (position - 1) + | res -> res + + match quickParseInfo with + | Some (islandColumn, qualifiers, textSpan) -> + let! res = checkFileResults.GetToolTipTextAlternate(textLineNumber, islandColumn, textLine.ToString(), qualifiers, FSharpTokenTag.IDENT) + return + match res with + | FSharpToolTipText [] -> None + | _ -> Some(res, textSpan) + | None -> return None + } + + interface IQuickInfoProvider with + override this.GetItemAsync(document: Document, position: int, cancellationToken: CancellationToken): Task = + async { + match FSharpLanguageService.GetOptions(document.Project.Id) with + | Some(options) -> + let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) + let classification = CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, cancellationToken) + + match classification with + | Some _ -> + let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask + let! quickInfoResult = FSharpQuickInfoProvider.ProvideQuickInfo(document.Id, sourceText, document.FilePath, position, options, textVersion.GetHashCode(), cancellationToken) + match quickInfoResult with + | Some(toolTipElement, textSpan) -> + let dataTipText = XmlDocumentation.BuildDataTipText(documentationBuilder, toolTipElement) + return QuickInfoItem(textSpan, FSharpDeferredQuickInfoContent(dataTipText)) + | None -> return null + | None -> return null + | None -> return null + } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) \ No newline at end of file diff --git a/vsintegration/tests/unittests/ColorizationServiceTests.fs b/vsintegration/tests/unittests/ColorizationServiceTests.fs index 7875f55cd71..6213835e3f8 100644 --- a/vsintegration/tests/unittests/ColorizationServiceTests.fs +++ b/vsintegration/tests/unittests/ColorizationServiceTests.fs @@ -18,7 +18,7 @@ type ColorizationServiceTests() = let textSpan = TextSpan(0, fileContents.Length) let fileName = if isScriptFile.IsSome && isScriptFile.Value then "test.fsx" else "test.fs" let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) - let tokens = FSharpColorizationService.GetColorizationData(documentId, SourceText.From(fileContents), textSpan, Some(fileName), defines, CancellationToken.None) + let tokens = CommonHelpers.getColorizationData(documentId, SourceText.From(fileContents), textSpan, Some(fileName), defines, CancellationToken.None) let markerPosition = fileContents.IndexOf(marker) Assert.IsTrue(markerPosition >= 0, "Cannot find marker '{0}' in file contents", marker) (tokens, markerPosition) diff --git a/vsintegration/tests/unittests/LanguageDebugInfoServiceTests.fs b/vsintegration/tests/unittests/LanguageDebugInfoServiceTests.fs index 736c18dd4a4..b904a120fbb 100644 --- a/vsintegration/tests/unittests/LanguageDebugInfoServiceTests.fs +++ b/vsintegration/tests/unittests/LanguageDebugInfoServiceTests.fs @@ -54,7 +54,7 @@ let main argv = let sourceText = SourceText.From(code) let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) - let tokens = FSharpColorizationService.GetColorizationData(documentId, sourceText, TextSpan.FromBounds(0, sourceText.Length), Some(fileName), defines, CancellationToken.None) + let tokens = CommonHelpers.getColorizationData(documentId, sourceText, TextSpan.FromBounds(0, sourceText.Length), Some(fileName), defines, CancellationToken.None) let actualDataTipSpanOption = FSharpLanguageDebugInfoService.GetDataTipInformation(sourceText, searchPosition, tokens) match actualDataTipSpanOption with diff --git a/vsintegration/tests/unittests/QuickInfoProviderTests.fs b/vsintegration/tests/unittests/QuickInfoProviderTests.fs new file mode 100644 index 00000000000..abe339bde75 --- /dev/null +++ b/vsintegration/tests/unittests/QuickInfoProviderTests.fs @@ -0,0 +1,103 @@ + +// To run the tests in this file: +// +// Technique 1: Compile VisualFSharp.Unittests.dll and run it as a set of unit tests +// +// Technique 2: +// +// Enable some tests in the #if EXE section at the end of the file, +// then compile this file as an EXE that has InternalsVisibleTo access into the +// appropriate DLLs. This can be the quickest way to get turnaround on updating the tests +// and capturing large amounts of structured output. +(* + cd Debug\net40\bin + .\fsc.exe --define:EXE -r:.\Microsoft.Build.Utilities.Core.dll -o VisualFSharp.Unittests.exe -g --optimize- -r .\FSharp.LanguageService.Compiler.dll -r .\FSharp.Editor.dll -r nunit.framework.dll ..\..\..\tests\service\FsUnit.fs ..\..\..\tests\service\Common.fs /delaysign /keyfile:..\..\..\src\fsharp\msft.pubkey ..\..\..\vsintegration\tests\unittests\CompletionProviderTests.fs + .\VisualFSharp.Unittests.exe +*) +// Technique 3: +// +// Use F# Interactive. This only works for FSharp.Compiler.Service.dll which has a public API + +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +module Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn.QuickInfoProviderTests + +open System +open System.Threading + +open NUnit.Framework + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor + +//open Microsoft.VisualStudio.FSharp.Editor +//open Microsoft.VisualStudio.FSharp.LanguageService + +open Microsoft.FSharp.Compiler.SourceCodeServices +//open Microsoft.FSharp.Compiler.Range + +let filePath = "C:\\test.fs" + +let internal options = { + ProjectFileName = "C:\\test.fsproj" + ProjectFileNames = [| filePath |] + ReferencedProjects = [| |] + OtherOptions = [| |] + IsIncompleteTypeCheckEnvironment = true + UseScriptResolutionRules = false + LoadTime = DateTime.MaxValue + UnresolvedReferences = None +} + +let private normalizeLineEnds (s: string) = s.Replace("\r\n", "\n").Replace("\n\n", "\n") + +let private getQuickInfoText (FSharpToolTipText elements) : string = + let rec parseElement = function + | FSharpToolTipElement.None -> "" + | FSharpToolTipElement.Single(text, _) -> text + | FSharpToolTipElement.SingleParameter(text, _, _) -> text + | FSharpToolTipElement.Group(xs) -> xs |> List.map fst |> String.concat "\n" + | FSharpToolTipElement.CompositionError(error) -> error + elements |> List.map parseElement |> String.concat "\n" |> normalizeLineEnds + +[] +let ShouldShowQuickInfoAtCorrectPositions() = + let testCases = + [ "x", Some "val x : int\nFull name: Test.x" + "y", Some "val y : int\nFull name: Test.y" + "1", None + "2", None + "x +", Some "val x : int\nFull name: Test.x" + "System", Some "namespace System" + "Console", Some "type Console = + static member BackgroundColor : ConsoleColor with get, set + static member Beep : unit -> unit + 1 overload + static member BufferHeight : int with get, set + static member BufferWidth : int with get, set + static member CapsLock : bool + static member Clear : unit -> unit + static member CursorLeft : int with get, set + static member CursorSize : int with get, set + static member CursorTop : int with get, set + static member CursorVisible : bool with get, set + ... +Full name: System.Console" + "WriteLine", Some "System.Console.WriteLine(value: int) : unit" ] + + for (symbol: string, expected: string option) in testCases do + let expected = expected |> Option.map normalizeLineEnds + let fileContents = """ + let x = 1 + let y = 2 + System.Console.WriteLine(x + y) + """ + let caretPosition = fileContents.IndexOf(symbol) + 1 + let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) + let getInfo() = documentId, filePath, [] + + let quickInfo = + FSharpQuickInfoProvider.ProvideQuickInfo(documentId, SourceText.From(fileContents), filePath, caretPosition, options, 0, CancellationToken.None) + |> Async.RunSynchronously + + let actual = quickInfo |> Option.map (fun (text, _) -> getQuickInfoText text) + Assert.AreEqual(expected, actual) \ No newline at end of file diff --git a/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs b/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs index 5176ed0eaab..4fcd2e6a977 100644 --- a/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs +++ b/vsintegration/tests/unittests/Tests.LanguageService.Completion.fs @@ -467,6 +467,22 @@ type UsingMSBuild() as this = AssertCtrlSpaceCompleteContains code "b a" ["aaa"; "bbb"] [] AssertCtrlSpaceCompleteContains code "a b" ["aaa"; "bbb"] [] + [] + member this.``AutoCompletion.OnTypeConstraintError``() = + let code = + [ + "type Foo = Foo" + " with" + " member __.Bar = 1" + " member __.PublicMethodForIntellisense() = 2" + " member internal __.InternalMethod() = 3" + " member private __.PrivateProperty = 4" + "" + "let u: Unit =" + " [ Foo ]" + " |> List.map (fun abcd -> abcd.)" + ] + AssertCtrlSpaceCompleteContains code "abcd." ["Bar"; "Equals"; "GetHashCode"; "GetType"; "InternalMethod"; "PublicMethodForIntellisense"; "ToString"] [] [] [] @@ -5750,7 +5766,7 @@ let rec f l = type IFoo = abstract DoStuff : unit -> string abstract DoStuff2 : int * int -> string -> string - // Implement an interface in a class (This is kind of lame if you don’t want to actually declare a class) + // Implement an interface in a class (This is kind of lame if you don't want to actually declare a class) type Foo() = interface IFoo with member this.DoStuff () = "Return a string" diff --git a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj index a6f016d6ad7..066282814b0 100644 --- a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj +++ b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj @@ -111,6 +111,9 @@ Roslyn\GoToDefinitionServiceTests.fs + + Roslyn\QuickInfoProvider\QuickInfoProviderTests.fs + VisualFSharp.Unittests.dll.config {VisualStudioVersion}