From 5b94f743275de25ab7ccef7111267c395f02588a Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Tue, 10 Oct 2023 11:01:56 +0200 Subject: [PATCH] More VS cleanup (#15954) Option -> ValueOption in some places More cancellable tasks Removed creating asyncs + running in parallel on lightweight CPU bound AIO find operations Something else. --- global.json | 3 +- .../BraceCompletionSessionProvider.fs | 29 +- .../Classification/ClassificationService.fs | 93 +- .../CodeFixes/AddOpenCodeFixProvider.fs | 6 +- .../Commands/XmlDocCommandService.fs | 9 +- .../FSharp.Editor/Common/CancellableTasks.fs | 13 +- .../Common/CodeAnalysisExtensions.fs | 13 +- .../src/FSharp.Editor/Common/DocumentCache.fs | 3 + .../src/FSharp.Editor/Common/Extensions.fs | 142 ++- .../src/FSharp.Editor/Common/Pervasive.fs | 51 +- .../src/FSharp.Editor/Common/RoslynHelpers.fs | 13 +- .../Completion/CompletionProvider.fs | 74 +- .../Debugging/BreakpointResolutionService.fs | 21 +- .../Diagnostics/DocumentDiagnosticAnalyzer.fs | 6 +- .../DocumentHighlightsService.fs | 4 +- .../src/FSharp.Editor/FSharp.Editor.fsproj | 2 +- .../Formatting/BraceMatchingService.fs | 4 +- .../InlineRename/InlineRenameService.fs | 8 +- .../AssemblyContentProvider.fs | 10 +- .../FSharpAnalysisSaveFileCommandHandler.fs | 62 +- .../FSharpProjectOptionsManager.fs | 87 +- .../LanguageService/LanguageService.fs | 8 +- .../LanguageService/SymbolHelpers.fs | 4 +- .../LanguageService/WorkspaceExtensions.fs | 38 +- .../Navigation/FindDefinitionService.fs | 12 +- .../Navigation/FindUsagesService.fs | 12 +- .../Navigation/GoToDefinition.fs | 869 +++++++++--------- .../Navigation/GoToDefinitionService.fs | 17 +- .../Navigation/NavigableSymbolsService.fs | 48 +- .../Navigation/NavigateToSearchService.fs | 89 +- .../Navigation/NavigationBarItemService.fs | 59 +- .../QuickInfo/QuickInfoProvider.fs | 4 +- .../Refactor/AddExplicitTypeToParameter.fs | 3 +- .../Refactor/ChangeDerefToValueRefactoring.fs | 3 + .../ChangeTypeofWithNameToNameofExpression.fs | 3 + .../Structure/BlockStructureService.fs | 19 +- .../Telemetry/TelemetryReporter.fs | 7 +- .../BreakpointResolutionServiceTests.fs | 4 +- .../CompletionProviderTests.fs | 4 +- .../FindReferencesTests.fs | 8 +- .../FsxCompletionProviderTests.fs | 2 +- 41 files changed, 1056 insertions(+), 810 deletions(-) diff --git a/global.json b/global.json index 8b3ca467ab8..1ffce949700 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,7 @@ { "sdk": { "version": "8.0.100-rc.1.23455.8", - "allowPrerelease": true, - "rollForward": "latestMajor" + "allowPrerelease": true }, "tools": { "dotnet": "8.0.100-rc.1.23455.8", diff --git a/vsintegration/src/FSharp.Editor/AutomaticCompletion/BraceCompletionSessionProvider.fs b/vsintegration/src/FSharp.Editor/AutomaticCompletion/BraceCompletionSessionProvider.fs index d9339a099d7..6c090529f2c 100644 --- a/vsintegration/src/FSharp.Editor/AutomaticCompletion/BraceCompletionSessionProvider.fs +++ b/vsintegration/src/FSharp.Editor/AutomaticCompletion/BraceCompletionSessionProvider.fs @@ -22,14 +22,11 @@ open Microsoft.CodeAnalysis.Classification [] module BraceCompletionSessionProviderHelpers = - let tryGetCaretPoint (buffer: ITextBuffer) (session: IBraceCompletionSession) = - let point = - session.TextView.Caret.Position.Point.GetPoint(buffer, PositionAffinity.Predecessor) + let inline tryGetCaretPoint (buffer: ITextBuffer) (session: IBraceCompletionSession) = + ValueOption.ofNullable (session.TextView.Caret.Position.Point.GetPoint(buffer, PositionAffinity.Predecessor)) - if point.HasValue then Some point.Value else None - - let tryGetCaretPosition session = - session |> tryGetCaretPoint session.SubjectBuffer + let inline tryGetCaretPosition (session: IBraceCompletionSession) = + tryGetCaretPoint session.SubjectBuffer session let tryInsertAdditionalBracePair (session: IBraceCompletionSession) openingChar closingChar = let sourceCode = session.TextView.TextSnapshot @@ -134,13 +131,13 @@ type BraceCompletionSession // exit without setting the closing point which will take us off the stack edit.Cancel() undo.Cancel() - None + ValueNone else - Some(edit.Apply()) // FIXME: perhaps, it should be ApplyAndLogExceptions() + ValueSome(edit.Apply()) // FIXME: perhaps, it should be ApplyAndLogExceptions() match nextSnapshot with - | None -> () - | Some (nextSnapshot) -> + | ValueNone -> () + | ValueSome (nextSnapshot) -> let beforePoint = beforeTrackingPoint.GetPoint(textView.TextSnapshot) @@ -185,7 +182,7 @@ type BraceCompletionSession if closingSnapshotPoint.Position > 0 then match tryGetCaretPosition this with - | Some caretPos when not (this.HasNoForwardTyping(caretPos, closingSnapshotPoint.Subtract(1))) -> true + | ValueSome caretPos when not (this.HasNoForwardTyping(caretPos, closingSnapshotPoint.Subtract(1))) -> true | _ -> false else false @@ -265,7 +262,7 @@ type BraceCompletionSession match caretPos with // ensure that we are within the session before clearing - | Some caretPos when + | ValueSome caretPos when caretPos.Position < closingSnapshotPoint.Position && closingSnapshotPoint.Position > 0 -> @@ -310,7 +307,7 @@ type BraceCompletionSession member this.PostReturn() = match tryGetCaretPosition this with - | Some caretPos -> + | ValueSome caretPos -> let closingSnapshotPoint = closingPoint.GetPoint(subjectBuffer.CurrentSnapshot) if @@ -580,13 +577,13 @@ type BraceCompletionSessionProvider [] maybe { let! document = openingPoint.Snapshot.GetOpenDocumentInCurrentContextWithChanges() - |> Option.ofObj + |> ValueOption.ofObj let! sessionFactory = document.TryGetLanguageService() let! session = sessionFactory.TryCreateSession(document, openingPoint.Position, openingBrace, CancellationToken.None) - |> Option.ofObj + |> ValueOption.ofObj let undoHistory = undoManager.GetTextBufferUndoManager(textView.TextBuffer).TextBufferUndoHistory diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index b837385c1a9..28c6e96220c 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -52,7 +52,7 @@ type internal FSharpClassificationService [] () = ClassificationTypeNames.Text match RoslynHelpers.TryFSharpRangeToTextSpan(text, tok.Range) with - | Some span -> result.Add(ClassifiedSpan(TextSpan(textSpan.Start + span.Start, span.Length), spanKind)) + | ValueSome span -> result.Add(ClassifiedSpan(TextSpan(textSpan.Start + span.Start, span.Length), spanKind)) | _ -> () let flags = @@ -79,8 +79,8 @@ type internal FSharpClassificationService [] () = = for item in items do match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) with - | None -> () - | Some span -> + | ValueNone -> () + | ValueSome span -> let span = match item.Type with | SemanticClassificationType.Printf -> span @@ -111,7 +111,7 @@ type internal FSharpClassificationService [] () = | true, items -> items | _ -> let items = ResizeArray() - lookup.[dataItem.Range.StartLine] <- items + lookup[dataItem.Range.StartLine] <- items items items.Add dataItem @@ -120,8 +120,29 @@ type internal FSharpClassificationService [] () = lookup :> IReadOnlyDictionary<_, _> - let semanticClassificationCache = - new DocumentCache("fsharp-semantic-classification-cache") + static let itemToSemanticClassificationLookup (d: SemanticClassificationItem array) = + let lookup = Dictionary>() + + for item in d do + let items = + let startLine = item.Range.StartLine + + match lookup.TryGetValue startLine with + | true, items -> items + | _ -> + let items = ResizeArray() + lookup[startLine] <- items + items + + items.Add item + + lookup :> IReadOnlyDictionary<_, _> + + static let unopenedDocumentsSemanticClassificationCache = + new DocumentCache("fsharp-unopened-documents-semantic-classification-cache", 5.) + + static let openedDocumentsSemanticClassificationCache = + new DocumentCache("fsharp-opened-documents-semantic-classification-cache", 2.) interface IFSharpClassificationService with // Do not perform classification if we don't have project options (#defines matter) @@ -197,7 +218,7 @@ type internal FSharpClassificationService [] () = let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id if not isOpenDocument then - match! semanticClassificationCache.TryGetValueAsync document with + match! unopenedDocumentsSemanticClassificationCache.TryGetValueAsync document with | ValueSome classificationDataLookup -> let eventProps: (string * obj) array = [| @@ -212,7 +233,7 @@ type internal FSharpClassificationService [] () = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticCalssifications, eventProps) addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result - | _ -> + | ValueNone -> let eventProps: (string * obj) array = [| "context.document.project.id", document.Project.Id.Id.ToString() @@ -226,29 +247,53 @@ type internal FSharpClassificationService [] () = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticCalssifications, eventProps) let! classificationData = document.GetFSharpSemanticClassificationAsync(nameof (FSharpClassificationService)) + let classificationDataLookup = toSemanticClassificationLookup classificationData - do! semanticClassificationCache.SetAsync(document, classificationDataLookup) + do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result else - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - "cacheHit", false - |] - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticCalssifications, eventProps) + match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with + | ValueSome classificationDataLookup -> + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", true + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticCalssifications, eventProps) + + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result + | ValueNone -> + + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", false + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticCalssifications, eventProps) + + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) + + let targetRange = + RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) + let classificationData = checkResults.GetSemanticClassification(Some targetRange) - let targetRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) + if classificationData.Length > 0 then + let classificationDataLookup = itemToSemanticClassificationLookup classificationData + do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) - let classificationData = checkResults.GetSemanticClassification(Some targetRange) - addSemanticClassification sourceText textSpan classificationData result + addSemanticClassification sourceText textSpan classificationData result } |> CancellableTask.startAsTask cancellationToken diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs index ac0b952f65f..e2db1a58dd4 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/AddOpenCodeFixProvider.fs @@ -188,8 +188,8 @@ type internal AddOpenCodeFixProvider [] (assemblyContentPr let entities = assemblyContentProvider.GetAllEntitiesInProjectAndReferencedAssemblies checkResults - |> List.collect (fun s -> - [ + |> Array.collect (fun s -> + [| yield s.TopRequireQualifiedAccessParent, s.AutoOpenParent, s.Namespace, s.CleanedIdents if isAttribute then let lastIdent = s.CleanedIdents.[s.CleanedIdents.Length - 1] @@ -204,7 +204,7 @@ type internal AddOpenCodeFixProvider [] (assemblyContentPr s.Namespace, s.CleanedIdents |> Array.replace (s.CleanedIdents.Length - 1) (lastIdent.Substring(0, lastIdent.Length - 9)) - ]) + |]) ParsedInput.GetLongIdentAt parseResults.ParseTree unresolvedIdentRange.End |> Option.bind (fun longIdent -> diff --git a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs index f2bcf18e870..5b762414c59 100644 --- a/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs +++ b/vsintegration/src/FSharp.Editor/Commands/XmlDocCommandService.fs @@ -16,6 +16,8 @@ open Microsoft.VisualStudio.TextManager.Interop open Microsoft.VisualStudio.LanguageServices open Microsoft.VisualStudio.Utilities open FSharp.Compiler.EditorServices +open CancellableTasks.CancellableTaskBuilder +open CancellableTasks type internal XmlDocCommandFilter(wpfTextView: IWpfTextView, filePath: string, workspace: VisualStudioWorkspace) = @@ -67,7 +69,12 @@ type internal XmlDocCommandFilter(wpfTextView: IWpfTextView, filePath: string, w let! document = getLastDocument () let! cancellationToken = Async.CancellationToken |> liftAsync let! sourceText = document.GetTextAsync(cancellationToken) - let! parseResults = document.GetFSharpParseResultsAsync(nameof (XmlDocCommandFilter)) |> liftAsync + + let! parseResults = + document.GetFSharpParseResultsAsync(nameof (XmlDocCommandFilter)) + |> CancellableTask.start cancellationToken + |> Async.AwaitTask + |> liftAsync let xmlDocables = XmlDocParser.GetXmlDocables(sourceText.ToFSharpSourceText(), parseResults.ParseTree) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 6e703fc2c59..1cd71d4228e 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -451,13 +451,13 @@ module CancellableTasks = /// /// Builds a cancellableTask using computation expression syntax. - /// Default behaviour when binding (v)options is to return a cacnelled task. + /// Default behaviour when binding (v)options is to return a cancelled task. /// let foregroundCancellableTask = CancellableTaskBuilder(false) /// /// Builds a cancellableTask using computation expression syntax which switches to execute on a background thread if not already doing so. - /// Default behaviour when binding (v)options is to return a cacnelled task. + /// Default behaviour when binding (v)options is to return a cancelled task. /// let cancellableTask = CancellableTaskBuilder(true) @@ -1096,17 +1096,18 @@ module CancellableTasks = let inline whenAll (tasks: CancellableTask<'a> seq) = cancellableTask { let! ct = getCancellationToken () - return! Task.WhenAll (seq { for task in tasks do yield start ct task }) + let tasks = seq { for task in tasks do yield start ct task } + return! Task.WhenAll (tasks) } let inline whenAllTasks (tasks: CancellableTask seq) = cancellableTask { let! ct = getCancellationToken () - return! Task.WhenAll (seq { for task in tasks do yield startTask ct task }) + let tasks = seq { for task in tasks do yield startTask ct task } + return! Task.WhenAll (tasks) } - let inline ignore (ctask: CancellableTask<_>) = - ctask |> toUnit + let inline ignore ([] ctask: CancellableTask<_>) = toUnit ctask /// [] diff --git a/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs index 888505ecda3..c42ace1a366 100644 --- a/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs @@ -53,17 +53,8 @@ type Solution with // It's crucial to normalize file path here (specificaly, remove relative parts), // otherwise Roslyn does not find documents. self.GetDocumentIdsWithFilePath(Path.GetFullPath filePath) - |> Seq.tryHead - |> Option.map (fun docId -> self.GetDocument docId) - - /// Try to find the document corresponding to the provided filepath and ProjectId within this solution - member self.TryGetDocumentFromPath(filePath, projId: ProjectId) = - // It's crucial to normalize file path here (specificaly, remove relative parts), - // otherwise Roslyn does not find documents. - self.GetDocumentIdsWithFilePath(Path.GetFullPath filePath) - |> Seq.filter (fun x -> x.ProjectId = projId) - |> Seq.tryHead - |> Option.map (fun docId -> self.GetDocument docId) + |> ImmutableArray.tryHeadV + |> ValueOption.map (fun docId -> self.GetDocument docId) /// Try to get a project inside the solution using the project's id member self.TryGetProject(projId: ProjectId) = diff --git a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs index f812e5f8b0f..6c88bc0f912 100644 --- a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs +++ b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs @@ -16,6 +16,9 @@ type DocumentCache<'Value when 'Value: not struct>(name: string, ?cacheItemPolic let policy = defaultArg cacheItemPolicy (CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds defaultSlidingExpiration))) + new(name: string, slidingExpirationSeconds: float) = + new DocumentCache<'Value>(name, CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds slidingExpirationSeconds))) + member _.TryGetValueAsync(doc: Document) = cancellableTask { let! ct = CancellableTask.getCancellationToken () diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 8b432dfdf7f..efcee384b99 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -303,7 +303,7 @@ module String = [] module Option = - let guard (x: bool) : Option = if x then Some() else None + let guard (x: bool) : ValueOption = if x then ValueSome() else ValueNone let attempt (f: unit -> 'T) = try @@ -332,11 +332,63 @@ module ValueOption = | ValueSome v -> Some v | _ -> None +[] +module IEnumerator = + let chooseV f (e: IEnumerator<'T>) = + let mutable started = false + let mutable curr = ValueNone + + let get () = + if not started then + raise (InvalidOperationException("Not started")) + + match curr with + | ValueNone -> raise (InvalidOperationException("Already finished")) + | ValueSome x -> x + + { new IEnumerator<'U> with + member _.Current = get () + interface System.Collections.IEnumerator with + member _.Current = box (get ()) + + member _.MoveNext() = + if not started then + started <- true + + curr <- ValueNone + + while (curr.IsNone && e.MoveNext()) do + curr <- f e.Current + + ValueOption.isSome curr + + member _.Reset() = + raise (NotSupportedException("Reset is not supported")) + interface System.IDisposable with + member _.Dispose() = e.Dispose() + } + [] module Seq = + let mkSeq f = + { new IEnumerable<'U> with + member _.GetEnumerator() = f () + interface System.Collections.IEnumerable with + member _.GetEnumerator() = + (f () :> System.Collections.IEnumerator) + } + + let inline revamp f (ie: seq<_>) = + mkSeq (fun () -> f (ie.GetEnumerator())) + let toImmutableArray (xs: seq<'a>) : ImmutableArray<'a> = xs.ToImmutableArray() + let inline tryHeadV (source: seq<_>) = + use e = source.GetEnumerator() + + if (e.MoveNext()) then ValueSome e.Current else ValueNone + let inline tryFindV ([] predicate) (source: seq<'T>) = use e = source.GetEnumerator() let mutable res = ValueNone @@ -360,6 +412,18 @@ module Seq = loop 0 + let inline tryPickV ([] chooser) (source: seq<'T>) = + use e = source.GetEnumerator() + let mutable res = ValueNone + + while (ValueOption.isNone res && e.MoveNext()) do + res <- chooser e.Current + + res + + let chooseV (chooser: 'a -> 'b voption) source = + revamp (IEnumerator.chooseV chooser) source + [] module Array = let inline foldi ([] folder: 'State -> int -> 'T -> 'State) (state: 'State) (xs: 'T[]) = @@ -374,6 +438,9 @@ module Array = let toImmutableArray (xs: 'T[]) = xs.ToImmutableArray() + let inline tryHeadV (array: _[]) = + if array.Length = 0 then ValueNone else ValueSome array[0] + let inline tryFindV ([] predicate) (array: _[]) = let rec loop i = @@ -383,6 +450,75 @@ module Array = loop 0 + let inline chooseV ([] chooser: 'T -> 'U voption) (array: 'T[]) = + + let mutable i = 0 + let mutable first = Unchecked.defaultof<'U> + let mutable found = false + + while i < array.Length && not found do + let element = array.[i] + + match chooser element with + | ValueNone -> i <- i + 1 + | ValueSome b -> + first <- b + found <- true + + if i <> array.Length then + + let chunk1: 'U[] = Array.zeroCreate ((array.Length >>> 2) + 1) + + chunk1.[0] <- first + let mutable count = 1 + i <- i + 1 + + while count < chunk1.Length && i < array.Length do + let element = array.[i] + + match chooser element with + | ValueNone -> () + | ValueSome b -> + chunk1.[count] <- b + count <- count + 1 + + i <- i + 1 + + if i < array.Length then + let chunk2: 'U[] = Array.zeroCreate (array.Length - i) + + count <- 0 + + while i < array.Length do + let element = array.[i] + + match chooser element with + | ValueNone -> () + | ValueSome b -> + chunk2.[count] <- b + count <- count + 1 + + i <- i + 1 + + let res: 'U[] = Array.zeroCreate (chunk1.Length + count) + + Array.Copy(chunk1, res, chunk1.Length) + Array.Copy(chunk2, 0, res, chunk1.Length, count) + res + else + Array.sub chunk1 0 count + else + Array.empty + +[] +module ImmutableArray = + let inline tryHeadV (xs: ImmutableArray<'T>) : 'T voption = + if xs.Length = 0 then ValueNone else ValueSome xs[0] + + let inline empty<'T> = ImmutableArray<'T>.Empty + + let inline create<'T> (x: 'T) = ImmutableArray.Create<'T>(x) + [] module List = let rec tryFindV predicate list = @@ -413,6 +549,10 @@ module Exception = |> flattenInner |> String.concat " ---> " +[] +module TextSpan = + let empty = TextSpan() + type Async with static member RunImmediateExceptOnUI(computation: Async<'T>, ?cancellationToken) = diff --git a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs index 18e5c463c72..bbd45cdbb43 100644 --- a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs +++ b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs @@ -6,18 +6,18 @@ open System.IO open System.Diagnostics /// Checks if the filePath ends with ".fsi" -let isSignatureFile (filePath: string) = +let inline isSignatureFile (filePath: string) = String.Equals(Path.GetExtension filePath, ".fsi", StringComparison.OrdinalIgnoreCase) /// Returns the corresponding signature file path for given implementation file path or vice versa -let getOtherFile (filePath: string) = +let inline getOtherFile (filePath: string) = if isSignatureFile filePath then Path.ChangeExtension(filePath, ".fs") else Path.ChangeExtension(filePath, ".fsi") /// Checks if the file paht ends with '.fsx' or '.fsscript' -let isScriptFile (filePath: string) = +let inline isScriptFile (filePath: string) = let ext = Path.GetExtension filePath String.Equals(ext, ".fsx", StringComparison.OrdinalIgnoreCase) @@ -42,7 +42,7 @@ type MaybeBuilder() = // (unit -> M<'T>) -> M<'T> [] - member _.Delay(f: unit -> 'T option) : 'T option = f () + member inline _.Delay([] f: unit -> 'T option) : 'T option = f () // M<'T> -> M<'T> -> M<'T> // or @@ -55,11 +55,18 @@ type MaybeBuilder() = // M<'T> * ('T -> M<'U>) -> M<'U> [] - member inline _.Bind(value, f: 'T -> 'U option) : 'U option = Option.bind f value + member inline _.Bind(value, [] f: 'T -> 'U option) : 'U option = Option.bind f value + + // M<'T> * ('T -> M<'U>) -> M<'U> + [] + member inline _.Bind(value: 'T voption, [] f: 'T -> 'U option) : 'U option = + match value with + | ValueNone -> None + | ValueSome value -> f value // 'T * ('T -> M<'U>) -> M<'U> when 'U :> IDisposable [] - member _.Using(resource: ('T :> System.IDisposable), body: _ -> _ option) : _ option = + member inline _.Using(resource: ('T :> System.IDisposable), [] body: _ -> _ option) : _ option = try body resource finally @@ -79,28 +86,28 @@ type MaybeBuilder() = // or // seq<'T> * ('T -> M<'U>) -> seq> [] - member x.For(sequence: seq<_>, body: 'T -> unit option) : _ option = + member inline x.For(sequence: seq<_>, [] body: 'T -> unit option) : _ option = // OPTIMIZE: This could be simplified so we don't need to make calls to Using, While, Delay. x.Using(sequence.GetEnumerator(), (fun enum -> x.While(enum.MoveNext, x.Delay(fun () -> body enum.Current)))) let maybe = MaybeBuilder() -[] +[] type AsyncMaybeBuilder() = [] - member _.Return value : Async<'T option> = Some value |> async.Return + member inline _.Return value : Async<'T option> = async.Return(Some value) [] - member _.ReturnFrom value : Async<'T option> = value + member inline _.ReturnFrom value : Async<'T option> = value [] - member _.ReturnFrom(value: 'T option) : Async<'T option> = async.Return value + member inline _.ReturnFrom(value: 'T option) : Async<'T option> = async.Return value [] - member _.Zero() : Async = Some() |> async.Return + member inline _.Zero() : Async = async.Return(Some()) [] - member _.Delay(f: unit -> Async<'T option>) : Async<'T option> = async.Delay f + member inline _.Delay([] f: unit -> Async<'T option>) : Async<'T option> = async.Delay f [] member _.Combine(r1, r2: Async<'T option>) : Async<'T option> = @@ -113,7 +120,7 @@ type AsyncMaybeBuilder() = } [] - member _.Bind(value: Async<'T option>, f: 'T -> Async<'U option>) : Async<'U option> = + member inline _.Bind(value: Async<'T option>, [] f: 'T -> Async<'U option>) : Async<'U option> = async { let! value' = value @@ -123,14 +130,14 @@ type AsyncMaybeBuilder() = } [] - member _.Bind(value: System.Threading.Tasks.Task<'T>, f: 'T -> Async<'U option>) : Async<'U option> = + member inline _.Bind(value: System.Threading.Tasks.Task<'T>, [] f: 'T -> Async<'U option>) : Async<'U option> = async { let! value' = Async.AwaitTask value return! f value' } [] - member _.Bind(value: 'T option, f: 'T -> Async<'U option>) : Async<'U option> = + member inline _.Bind(value: 'T option, [] f: 'T -> Async<'U option>) : Async<'U option> = async { match value with | None -> return None @@ -138,7 +145,15 @@ type AsyncMaybeBuilder() = } [] - member _.Using(resource: ('T :> IDisposable), body: 'T -> Async<'U option>) : Async<'U option> = + member inline _.Bind(value: 'T voption, [] f: 'T -> Async<'U option>) : Async<'U option> = + async { + match value with + | ValueNone -> return None + | ValueSome result -> return! f result + } + + [] + member inline _.Using(resource: ('T :> IDisposable), [] body: 'T -> Async<'U option>) : Async<'U option> = async { use resource = resource return! body resource @@ -152,7 +167,7 @@ type AsyncMaybeBuilder() = x.Zero() [] - member x.For(sequence: seq<_>, body: 'T -> Async) : Async = + member inline x.For(sequence: seq<_>, [] body: 'T -> Async) : Async = x.Using(sequence.GetEnumerator(), (fun enum -> x.While(enum.MoveNext, x.Delay(fun () -> body enum.Current)))) [] diff --git a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs index 41dee649d81..eec57d2a238 100644 --- a/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/RoslynHelpers.fs @@ -46,12 +46,12 @@ module internal RoslynHelpers = TextSpan(startPosition, endPosition - startPosition) - let TryFSharpRangeToTextSpan (sourceText: SourceText, range: range) : TextSpan option = + let TryFSharpRangeToTextSpan (sourceText: SourceText, range: range) : TextSpan voption = try - Some(FSharpRangeToTextSpan(sourceText, range)) + ValueSome(FSharpRangeToTextSpan(sourceText, range)) with e -> //Assert.Exception(e) - None + ValueNone let TextSpanToFSharpRange (fileName: string, textSpan: TextSpan, sourceText: SourceText) : range = let startLine = sourceText.Lines.GetLineFromPosition textSpan.Start @@ -62,13 +62,6 @@ module internal RoslynHelpers = (Position.fromZ startLine.LineNumber (textSpan.Start - startLine.Start)) (Position.fromZ endLine.LineNumber (textSpan.End - endLine.Start)) - let GetCompletedTaskResult (task: Task<'TResult>) = - if task.Status = TaskStatus.RanToCompletion then - task.Result - else - Assert.Exception(task.Exception.GetBaseException()) - raise (task.Exception.GetBaseException()) - /// maps from `TextTag` of the F# Compiler to Roslyn `TextTags` for use in tooltips let roslynTag = function diff --git a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs index d0ce229f64b..7d121bf4ab6 100644 --- a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs @@ -150,14 +150,14 @@ type internal FSharpCompletionProvider ( document: Document, caretPosition: int, - getAllSymbols: FSharpCheckFileResults -> AssemblySymbol list + getAllSymbols: FSharpCheckFileResults -> AssemblySymbol array ) = cancellableTask { - let! parseResults, checkFileResults = document.GetFSharpParseAndCheckResultsAsync("ProvideCompletionsAsyncAux") - let! ct = CancellableTask.getCancellationToken () + let! parseResults, checkFileResults = document.GetFSharpParseAndCheckResultsAsync("ProvideCompletionsAsyncAux") + let! sourceText = document.GetTextAsync(ct) let textLines = sourceText.Lines let caretLinePos = textLines.GetLinePosition(caretPosition) @@ -194,9 +194,8 @@ type internal FSharpCompletionProvider let results = List() - declarationItems <- - declarations.Items - |> Array.sortWith (fun x y -> + Array.sortInPlaceWith + (fun (x: DeclarationListItem) (y: DeclarationListItem) -> let mutable n = (not x.IsResolved).CompareTo(not y.IsResolved) if n <> 0 then @@ -220,6 +219,9 @@ type internal FSharpCompletionProvider n else x.MinorPriority.CompareTo(y.MinorPriority)) + declarations.Items + + declarationItems <- declarations.Items let maxHints = if mruItems.Values.Count = 0 then @@ -227,23 +229,20 @@ type internal FSharpCompletionProvider else Seq.max mruItems.Values - declarationItems - |> Array.iteri (fun number declarationItem -> + for number = 0 to declarationItems.Length - 1 do + let declarationItem = declarationItems[number] + let glyph = Tokenizer.FSharpGlyphToRoslynGlyph(declarationItem.Glyph, declarationItem.Accessibility) - let namespaceName = - match declarationItem.NamespaceToOpen with - | Some namespaceToOpen -> namespaceToOpen - | _ -> null // Icky, but this is how roslyn handles it - - let filterText = + let namespaceName, filterText = match declarationItem.NamespaceToOpen, declarationItem.NameInList.Split '.' with // There is no namespace to open and the item name does not contain dots, so we don't need to pass special FilterText to Roslyn. - | None, [| _ |] -> null + | None, [| _ |] -> null, null + | Some namespaceToOpen, idents -> namespaceToOpen, Array.last idents // Either we have a namespace to open ("DateTime (open System)") or item name contains dots ("Array.map"), or both. // We are passing last part of long ident as FilterText. - | _, idents -> Array.last idents + | None, idents -> null, Array.last idents let completionItem = FSharpCommonCompletionItem @@ -282,7 +281,7 @@ type internal FSharpCompletionProvider let sortText = priority.ToString("D6") let completionItem = completionItem.WithSortText(sortText) - results.Add(completionItem)) + results.Add(completionItem) if results.Count > 0 @@ -352,11 +351,11 @@ type internal FSharpCompletionProvider ) if shouldProvideCompetion then - let getAllSymbols (fileCheckResults: FSharpCheckFileResults) = + let inline getAllSymbols (fileCheckResults: FSharpCheckFileResults) = if settings.IntelliSense.IncludeSymbolsFromUnopenedNamespacesOrModules then assemblyContentProvider.GetAllEntitiesInProjectAndReferencedAssemblies(fileCheckResults) else - [] + Array.empty let! results = FSharpCompletionProvider.ProvideCompletionsAsyncAux(context.Document, context.Position, getAllSymbols) @@ -369,31 +368,26 @@ type internal FSharpCompletionProvider ( document: Document, completionItem: Completion.CompletionItem, - cancellationToken: CancellationToken + _cancellationToken: CancellationToken ) : Task = match completionItem.Properties.TryGetValue IndexPropName with | true, completionItemIndexStr when int completionItemIndexStr >= declarationItems.Length -> Task.FromResult CompletionDescription.Empty | true, completionItemIndexStr -> - // TODO: Not entirely sure why do we use tasks here, since everything here is synchronous. - cancellableTask { - use _logBlock = - Logger.LogBlockMessage document.Name LogEditorFunctionId.Completion_GetDescriptionAsync - - let completionItemIndex = int completionItemIndexStr + use _logBlock = + Logger.LogBlockMessage document.Name LogEditorFunctionId.Completion_GetDescriptionAsync - let declarationItem = declarationItems.[completionItemIndex] - let description = declarationItem.Description - let documentation = List() - let collector = RoslynHelpers.CollectTaggedText documentation - // mix main description and xmldoc by using one collector - XmlDocumentation.BuildDataTipText(documentationBuilder, collector, collector, collector, collector, collector, description) + let completionItemIndex = int completionItemIndexStr - return CompletionDescription.Create(documentation.ToImmutableArray()) - } - |> CancellableTask.start cancellationToken + let declarationItem = declarationItems.[completionItemIndex] + let description = declarationItem.Description + let documentation = List() + let collector = RoslynHelpers.CollectTaggedText documentation + // mix main description and xmldoc by using one collector + XmlDocumentation.BuildDataTipText(documentationBuilder, collector, collector, collector, collector, collector, description) + Task.FromResult(CompletionDescription.Create(documentation.ToImmutableArray())) | _ -> match completionItem.Properties.TryGetValue KeywordDescription with | true, keywordDescription -> Task.FromResult(CompletionDescription.FromText(keywordDescription)) @@ -406,8 +400,8 @@ type internal FSharpCompletionProvider let fullName = match item.Properties.TryGetValue FullNamePropName with - | true, x -> Some x - | _ -> None + | true, x -> ValueSome x + | _ -> ValueNone // do not add extension members and unresolved symbols to the MRU list if @@ -415,7 +409,7 @@ type internal FSharpCompletionProvider && not (item.Properties.ContainsKey IsExtensionMemberPropName) then match fullName with - | Some fullName -> + | ValueSome fullName -> match mruItems.TryGetValue fullName with | true, hints -> mruItems.[fullName] <- hints + 1 | _ -> mruItems.[fullName] <- 1 @@ -439,7 +433,9 @@ type internal FSharpCompletionProvider let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpCompletionProvider)) let fullNameIdents = - fullName |> Option.map (fun x -> x.Split '.') |> Option.defaultValue [||] + fullName + |> ValueOption.map (fun x -> x.Split '.') + |> ValueOption.defaultValue [||] let insertionPoint = if settings.CodeFixes.AlwaysPlaceOpensAtTopLevel then diff --git a/vsintegration/src/FSharp.Editor/Debugging/BreakpointResolutionService.fs b/vsintegration/src/FSharp.Editor/Debugging/BreakpointResolutionService.fs index d73b1a61d05..56ad5a26518 100644 --- a/vsintegration/src/FSharp.Editor/Debugging/BreakpointResolutionService.fs +++ b/vsintegration/src/FSharp.Editor/Debugging/BreakpointResolutionService.fs @@ -33,18 +33,17 @@ type internal FSharpBreakpointResolutionService [] () = sourceText.GetSubText(sourceText.Lines.[textLinePos.Line].Span).ToString() if String.IsNullOrWhiteSpace textInLine then - return None + return ValueNone else let textLineColumn = textLinePos.Character let fcsTextLineNumber = Line.fromZ textLinePos.Line // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based - let! parseResults = - document.GetFSharpParseResultsAsync(nameof (FSharpBreakpointResolutionService)) - |> liftAsync + let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpBreakpointResolutionService)) - match parseResults with - | Some parseResults -> return parseResults.ValidateBreakpointLocation(mkPos fcsTextLineNumber textLineColumn) - | _ -> return None + let location = + parseResults.ValidateBreakpointLocation(mkPos fcsTextLineNumber textLineColumn) + + return ValueOption.ofOption location } interface IFSharpBreakpointResolutionService with @@ -58,14 +57,14 @@ type internal FSharpBreakpointResolutionService [] () = let! range = FSharpBreakpointResolutionService.GetBreakpointLocation(document, textSpan) match range with - | None -> return Unchecked.defaultof<_> - | Some range -> + | ValueNone -> return Unchecked.defaultof<_> + | ValueSome range -> let! sourceText = document.GetTextAsync(cancellationToken) let span = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) match span with - | None -> return Unchecked.defaultof<_> - | Some span -> return FSharpBreakpointResolutionResult.CreateSpanResult(document, span) + | ValueNone -> return Unchecked.defaultof<_> + | ValueSome span -> return FSharpBreakpointResolutionResult.CreateSpanResult(document, span) } |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 4329596ea49..0b6bb92a1c8 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -16,7 +16,7 @@ open FSharp.Compiler.Diagnostics open CancellableTasks open Microsoft.VisualStudio.FSharp.Editor.Telemetry -[] +[] type internal DiagnosticsType = | Syntax | Semantic @@ -144,12 +144,12 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = if document.Project.IsFSharpMetadata then Task.FromResult ImmutableArray.Empty else - cancellableTask { return! FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Syntax) } + FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Syntax) |> CancellableTask.start cancellationToken member _.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken) : Task> = if document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript then Task.FromResult ImmutableArray.Empty else - cancellableTask { return! FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Semantic) } + FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Semantic) |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs index aaf2a8a5db1..7947b2dec99 100644 --- a/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs +++ b/vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs @@ -97,8 +97,8 @@ type internal FSharpDocumentHighlightsService [] () = [| for symbolUse in symbolUses do match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) with - | None -> () - | Some span -> + | ValueNone -> () + | ValueSome span -> yield { IsDefinition = symbolUse.IsFromDefinition diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index b2a685d2347..7b9162b1f24 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -18,7 +18,7 @@ - + true Microsoft.VisualStudio.FSharp.Editor.SR diff --git a/vsintegration/src/FSharp.Editor/Formatting/BraceMatchingService.fs b/vsintegration/src/FSharp.Editor/Formatting/BraceMatchingService.fs index 11fa356b1f4..1b58774e7a1 100644 --- a/vsintegration/src/FSharp.Editor/Formatting/BraceMatchingService.fs +++ b/vsintegration/src/FSharp.Editor/Formatting/BraceMatchingService.fs @@ -28,8 +28,8 @@ type internal FSharpBraceMatchingService [] () = let isPositionInRange range = match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with - | None -> false - | Some span -> + | ValueNone -> false + | ValueSome span -> if forFormatting then let length = position - span.Start length >= 0 && length <= span.Length diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index 0cb40be0b10..d63e2d016df 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -167,10 +167,10 @@ type internal InlineRenameInfo [| for symbolUse in symbolUses do match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with - | Some span -> + | ValueSome span -> let textSpan = Tokenizer.fixupSpan (sourceText, span) yield FSharpInlineRenameLocation(document, textSpan) - | None -> () + | ValueNone -> () |] } } @@ -220,8 +220,8 @@ type internal InlineRenameService [] () = let span = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) match span with - | None -> return Unchecked.defaultof<_> - | Some span -> + | ValueNone -> return Unchecked.defaultof<_> + | ValueSome span -> let triggerSpan = Tokenizer.fixupSpan (sourceText, span) let result = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/AssemblyContentProvider.fs b/vsintegration/src/FSharp.Editor/LanguageService/AssemblyContentProvider.fs index f34ca68a2dc..7a4b39c6737 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/AssemblyContentProvider.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/AssemblyContentProvider.fs @@ -12,8 +12,8 @@ open FSharp.Compiler.EditorServices type internal AssemblyContentProvider() = let entityCache = EntityCache() - member x.GetAllEntitiesInProjectAndReferencedAssemblies(fileCheckResults: FSharpCheckFileResults) = - [ + member _.GetAllEntitiesInProjectAndReferencedAssemblies(fileCheckResults: FSharpCheckFileResults) = + [| yield! AssemblyContent.GetAssemblySignatureContent AssemblyContentType.Full fileCheckResults.PartialAssemblySignature // FCS sometimes returns several FSharpAssembly for single referenced assembly. // For example, it returns two different ones for Swensen.Unquote; the first one @@ -24,11 +24,9 @@ type internal AssemblyContentProvider() = fileCheckResults.ProjectContext.GetReferencedAssemblies() |> Seq.groupBy (fun asm -> asm.FileName) |> Seq.map (fun (fileName, asms) -> fileName, List.ofSeq asms) - |> Seq.toList - |> List.rev // if mscorlib.dll is the first then FSC raises exception when we try to - // get Content.Entities from it. + |> Seq.rev // if mscorlib.dll is the first then FSC raises exception when we try to get Content.Entities from it. for fileName, signatures in assembliesByFileName do let contentType = AssemblyContentType.Public // it's always Public for now since we don't support InternalsVisibleTo attribute yet yield! AssemblyContent.GetAssemblyContent entityCache.Locking contentType fileName signatures - ] + |] diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs index 30baf56eb3e..305cd868921 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpAnalysisSaveFileCommandHandler.fs @@ -12,6 +12,8 @@ open Microsoft.VisualStudio.FSharp.Editor.Logging open Microsoft.VisualStudio.Text.Editor.Commanding.Commands open Microsoft.VisualStudio.Commanding open Microsoft.VisualStudio.Utilities +open CancellableTasks +open Microsoft.VisualStudio.FSharp.Editor.Telemetry // This causes re-analysis to happen when a F# document is saved. // We do this because FCS relies on the file system and existing open documents @@ -46,42 +48,52 @@ type internal FSharpAnalysisSaveFileCommandHandler [] (ana | _ -> let document = solution.GetDocument(documentId) - async { - try - if document.Project.Language = LanguageNames.FSharp then + if document.Project.Language <> LanguageNames.FSharp then + () + else + cancellableTask { + try let openDocIds = workspace.GetOpenDocumentIds() let docIdsToReanalyze = if document.IsFSharpScript then - openDocIds - |> Seq.filter (fun x -> - x <> document.Id - && (let doc = solution.GetDocument(x) - - match doc with - | null -> false - | _ -> doc.IsFSharpScript)) - |> Array.ofSeq + [| + for x in openDocIds do + if + x <> document.Id + && (let doc = solution.GetDocument(x) + + match doc with + | null -> false + | _ -> doc.IsFSharpScript) + then + yield x + |] else let depProjIds = document.Project.GetDependentProjectIds().Add(document.Project.Id) - openDocIds - |> Seq.filter (fun x -> - depProjIds.Contains(x.ProjectId) - && x <> document.Id - && (let doc = solution.GetDocument(x) + [| + for x in openDocIds do + if + depProjIds.Contains(x.ProjectId) + && x <> document.Id + && (let doc = solution.GetDocument(x) - match box doc with - | null -> false - | _ -> doc.Project.Language = LanguageNames.FSharp)) - |> Array.ofSeq + match box doc with + | null -> false + | _ -> doc.Project.Language = LanguageNames.FSharp) + then + yield x + |] if docIdsToReanalyze.Length > 0 then analyzerService.Reanalyze(workspace, documentIds = docIdsToReanalyze) - with ex -> - logException ex - } - |> Async.Start // fire and forget + with ex -> + TelemetryReporter.ReportFault(TelemetryEvents.AnalysisSaveFileHandler, e = ex) + logException ex + } + |> CancellableTask.startWithoutCancellation + |> ignore // fire and forget nextCommandHandler.Invoke() diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 345bd3d707f..4fcc180201b 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -15,6 +15,7 @@ open Microsoft.VisualStudio.FSharp.Editor open System.Threading open Microsoft.VisualStudio.FSharp.Interactive.Session open System.Runtime.CompilerServices +open CancellableTasks [] module private FSharpProjectOptionsHelpers = @@ -55,7 +56,7 @@ module private FSharpProjectOptionsHelpers = and set (v) = errorReporter <- v } - let hasProjectVersionChanged (oldProject: Project) (newProject: Project) = + let inline hasProjectVersionChanged (oldProject: Project) (newProject: Project) = oldProject.Version <> newProject.Version let hasDependentVersionChanged (oldProject: Project) (newProject: Project) (ct: CancellationToken) = @@ -97,10 +98,10 @@ module private FSharpProjectOptionsHelpers = type private FSharpProjectOptionsMessage = | TryGetOptionsByDocument of Document * - AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) option> * + AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) voption> * CancellationToken * userOpName: string - | TryGetOptionsByProject of Project * AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) option> * CancellationToken + | TryGetOptionsByProject of Project * AsyncReplyChannel<(FSharpParsingOptions * FSharpProjectOptions) voption> * CancellationToken | ClearOptions of ProjectId | ClearSingleFileOptionsCache of DocumentId @@ -188,13 +189,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = weakPEReferences.Add(comp, fsRefProj) fsRefProj - let rec tryComputeOptionsBySingleScriptOrFile (document: Document) (ct: CancellationToken) userOpName = - async { - let! fileStamp = document.GetTextVersionAsync(ct) |> Async.AwaitTask + let rec tryComputeOptionsBySingleScriptOrFile (document: Document) userOpName = + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! fileStamp = document.GetTextVersionAsync(ct) match singleFileCache.TryGetValue(document.Id) with | false, _ -> - let! sourceText = document.GetTextAsync(ct) |> Async.AwaitTask + let! sourceText = document.GetTextAsync(ct) let! scriptProjectOptions, _ = checker.GetProjectOptionsFromScript( @@ -243,29 +245,30 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = singleFileCache.[document.Id] <- (document.Project, fileStamp, parsingOptions, projectOptions) - return Some(parsingOptions, projectOptions) + return ValueSome(parsingOptions, projectOptions) | true, (oldProject, oldFileStamp, parsingOptions, projectOptions) -> if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject ct then singleFileCache.TryRemove(document.Id) |> ignore - return! tryComputeOptionsBySingleScriptOrFile document ct userOpName + return! tryComputeOptionsBySingleScriptOrFile document userOpName else - return Some(parsingOptions, projectOptions) + return ValueSome(parsingOptions, projectOptions) } let tryGetProjectSite (project: Project) = // Cps if commandLineOptions.ContainsKey project.Id then - Some(mapCpsProjectToSite (project, commandLineOptions)) + ValueSome(mapCpsProjectToSite (project, commandLineOptions)) else // Legacy match legacyProjectSites.TryGetValue project.Id with - | true, site -> Some site - | _ -> None + | true, site -> ValueSome site + | _ -> ValueNone - let rec tryComputeOptions (project: Project) ct = - async { + let rec tryComputeOptions (project: Project) = + cancellableTask { let projectId = project.Id + let! ct = CancellableTask.getCancellationToken () match cache.TryGetValue(projectId) with | false, _ -> @@ -281,24 +284,23 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let referencedProject = project.Solution.GetProject(projectReference.ProjectId) if referencedProject.Language = FSharpConstants.FSharpLanguageName then - match! tryComputeOptions referencedProject ct with - | None -> canBail <- true - | Some (_, projectOptions) -> + match! tryComputeOptions referencedProject with + | ValueNone -> canBail <- true + | ValueSome (_, projectOptions) -> referencedProjects.Add( FSharpReferencedProject.FSharpReference(referencedProject.OutputFilePath, projectOptions) ) elif referencedProject.SupportsCompilation then - let! comp = referencedProject.GetCompilationAsync(ct) |> Async.AwaitTask + let! comp = referencedProject.GetCompilationAsync(ct) let peRef = createPEReference referencedProject comp referencedProjects.Add(peRef) if canBail then - return None + return ValueNone else - match tryGetProjectSite project with - | None -> return None - | Some projectSite -> + | ValueNone -> return ValueNone + | ValueSome projectSite -> let otherOptions = [| @@ -321,7 +323,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = "--ignorelinedirectives" |] - let! ver = project.GetDependentVersionAsync(ct) |> Async.AwaitTask + let! ver = project.GetDependentVersionAsync(ct) let projectOptions = { @@ -340,7 +342,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = // This can happen if we didn't receive the callback from HandleCommandLineChanges yet. if Array.isEmpty projectOptions.SourceFiles then - return None + return ValueNone else // Clear any caches that need clearing and invalidate the project. let currentSolution = project.Solution.Workspace.CurrentSolution @@ -371,14 +373,14 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = cache.[projectId] <- (project, parsingOptions, projectOptions) - return Some(parsingOptions, projectOptions) + return ValueSome(parsingOptions, projectOptions) | true, (oldProject, parsingOptions, projectOptions) -> if isProjectInvalidated oldProject project ct then cache.TryRemove(projectId) |> ignore return! tryComputeOptions project ct else - return Some(parsingOptions, projectOptions) + return ValueSome(parsingOptions, projectOptions) } let loop (agent: MailboxProcessor) = @@ -387,17 +389,20 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match! agent.Receive() with | FSharpProjectOptionsMessage.TryGetOptionsByDocument (document, reply, ct, userOpName) -> if ct.IsCancellationRequested then - reply.Reply None + reply.Reply ValueNone else try // For now, disallow miscellaneous workspace since we are using the hacky F# miscellaneous files project. if document.Project.Solution.Workspace.Kind = WorkspaceKind.MiscellaneousFiles then - reply.Reply None + reply.Reply ValueNone elif document.Project.IsFSharpMiscellaneousOrMetadata then - let! options = tryComputeOptionsBySingleScriptOrFile document ct userOpName + let! options = + tryComputeOptionsBySingleScriptOrFile document userOpName + |> CancellableTask.start ct + |> Async.AwaitTask if ct.IsCancellationRequested then - reply.Reply None + reply.Reply ValueNone else reply.Reply options else @@ -407,43 +412,43 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = document.Project.Solution.Workspace.CurrentSolution.GetProject(document.Project.Id) if not (isNull project) then - let! options = tryComputeOptions project ct + let! options = tryComputeOptions project |> CancellableTask.start ct |> Async.AwaitTask if ct.IsCancellationRequested then - reply.Reply None + reply.Reply ValueNone else reply.Reply options else - reply.Reply None + reply.Reply ValueNone with _ -> - reply.Reply None + reply.Reply ValueNone | FSharpProjectOptionsMessage.TryGetOptionsByProject (project, reply, ct) -> if ct.IsCancellationRequested then - reply.Reply None + reply.Reply ValueNone else try if project.Solution.Workspace.Kind = WorkspaceKind.MiscellaneousFiles || project.IsFSharpMiscellaneousOrMetadata then - reply.Reply None + reply.Reply ValueNone else // We only care about the latest project in the workspace's solution. // We do this to prevent any possible cache thrashing in FCS. let project = project.Solution.Workspace.CurrentSolution.GetProject(project.Id) if not (isNull project) then - let! options = tryComputeOptions project ct + let! options = tryComputeOptions project |> CancellableTask.start ct |> Async.AwaitTask if ct.IsCancellationRequested then - reply.Reply None + reply.Reply ValueNone else reply.Reply options else - reply.Reply None + reply.Reply ValueNone with _ -> - reply.Reply None + reply.Reply ValueNone | FSharpProjectOptionsMessage.ClearOptions (projectId) -> match cache.TryRemove(projectId) with diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index ad5156e3082..0512a48c1ad 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -94,11 +94,13 @@ type internal FSharpWorkspaceServiceFactory [] let getSource filename = async { + let! ct = Async.CancellationToken + match workspace.CurrentSolution.TryGetDocumentFromPath filename with - | Some document -> - let! text = document.GetTextAsync() |> Async.AwaitTask + | ValueSome document -> + let! text = document.GetTextAsync(ct) |> Async.AwaitTask return Some(text.ToFSharpSourceText()) - | None -> return None + | ValueNone -> return None } lock gate (fun () -> diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index b567c74a199..9871d042f2f 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -110,12 +110,12 @@ module internal SymbolHelpers = let! otherFileCheckResults = match currentDocument.Project.Solution.TryGetDocumentFromPath otherFile with - | Some doc -> + | ValueSome doc -> cancellableTask { let! _, checkFileResults = doc.GetFSharpParseAndCheckResultsAsync("findReferencedSymbolsAsync") return [ checkFileResults, doc ] } - | None -> CancellableTask.singleton [] + | ValueNone -> CancellableTask.singleton [] let symbolUses = (checkFileResults, currentDocument) :: otherFileCheckResults diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index b8bc19cc4a3..ddc2e593b76 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -2,10 +2,7 @@ module internal Microsoft.VisualStudio.FSharp.Editor.WorkspaceExtensions open System -open System.Diagnostics open System.Runtime.CompilerServices -open System.Threading -open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.VisualStudio.FSharp.Editor @@ -15,9 +12,6 @@ open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks -open CancellableTasks -open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks - [] module private CheckerExtensions = @@ -25,9 +19,9 @@ module private CheckerExtensions = /// Parse the source text from the Roslyn document. member checker.ParseDocument(document: Document, parsingOptions: FSharpParsingOptions, userOpName: string) = - async { - let! ct = Async.CancellationToken - let! sourceText = document.GetTextAsync(ct) |> Async.AwaitTask + cancellableTask { + let! ct = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync(ct) return! checker.ParseFile(document.FilePath, sourceText.ToFSharpSourceText(), parsingOptions, userOpName = userOpName) } @@ -100,8 +94,7 @@ module private CheckerExtensions = return results else - let! results = parseAndCheckFile - return results + return! parseAndCheckFile } /// Parse and check the source text from the Roslyn document. @@ -152,8 +145,8 @@ type Document with let! ct = CancellableTask.getCancellationToken () match! projectOptionsManager.TryGetOptionsForDocumentOrProject(this, ct, userOpName) with - | None -> return raise (OperationCanceledException("FSharp project options not found.")) - | Some (parsingOptions, projectOptions) -> + | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + | ValueSome (parsingOptions, projectOptions) -> let result = (service.Checker, projectOptionsManager, parsingOptions, projectOptions) @@ -161,13 +154,6 @@ type Document with ProjectCache.Projects.GetValue(this.Project, ConditionalWeakTable<_, _>.CreateValueCallback (fun _ -> result)) } - /// Get the compilation defines from F# project that is associated with the given F# document. - member this.GetFSharpCompilationDefinesAsync(userOpName) = - async { - let! _, _, parsingOptions, _ = this.GetFSharpCompilationOptionsAsync(userOpName) - return CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions - } - /// Get the compilation defines and language version from F# project that is associated with the given F# document. member this.GetFsharpParsingOptionsAsync(userOpName) = async { @@ -209,7 +195,7 @@ type Document with /// Parses the given F# document. member this.GetFSharpParseResultsAsync(userOpName) = - async { + cancellableTask { let! checker, _, parsingOptions, _ = this.GetFSharpCompilationOptionsAsync(userOpName) return! checker.ParseDocument(this, parsingOptions, userOpName) } @@ -258,10 +244,10 @@ type Document with /// Try to find a F# lexer/token symbol of the given F# document and position. member this.TryFindFSharpLexerSymbolAsync(position, lookupKind, wholeActivePattern, allowStringToken, userOpName) = - async { + cancellableTask { let! defines, langVersion, strictIndentation = this.GetFsharpParsingOptionsAsync(userOpName) - let! ct = Async.CancellationToken - let! sourceText = this.GetTextAsync(ct) |> Async.AwaitTask + let! ct = CancellableTask.getCancellationToken () + let! sourceText = this.GetTextAsync(ct) return Tokenizer.getSymbolAtPosition ( @@ -344,8 +330,8 @@ type Project with let projectOptionsManager = service.FSharpProjectOptionsManager match! projectOptionsManager.TryGetOptionsByProject(this, ct) with - | None -> return raise (OperationCanceledException("FSharp project options not found.")) - | Some (parsingOptions, projectOptions) -> + | ValueNone -> return raise (OperationCanceledException("FSharp project options not found.")) + | ValueSome (parsingOptions, projectOptions) -> let result = (service.Checker, projectOptionsManager, parsingOptions, projectOptions) diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs index 1864e99c508..bc408c01741 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindDefinitionService.fs @@ -10,15 +10,15 @@ open FSharp.Compiler.Text.Range open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.GoToDefinition -open System.Collections.Immutable -open System.Threading.Tasks +open CancellableTasks [)>] [)>] type internal FSharpFindDefinitionService [] (metadataAsSource: FSharpMetadataAsSourceService) = interface IFSharpFindDefinitionService with member _.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = - let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) - - let definitions = navigation.FindDefinitions(position, cancellationToken) // TODO: probably will need to be async all the way down - ImmutableArray.CreateRange(definitions) |> Task.FromResult + cancellableTask { + let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) + return! navigation.FindDefinitionsAsync(position) + } + |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs index 88aad129dc5..429b21ce64a 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs @@ -33,8 +33,8 @@ module FSharpFindUsagesService = match declarationRange, RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with | Some declRange, _ when Range.equals declRange symbolUse -> () - | _, None -> () - | _, Some textSpan -> + | _, ValueNone -> () + | _, ValueSome textSpan -> if allReferences then let definitionItem = if isExternal then @@ -71,10 +71,10 @@ module FSharpFindUsagesService = let! sourceText = doc.GetTextAsync(cancellationToken) match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with - | Some span -> + | ValueSome span -> let span = Tokenizer.fixupSpan (sourceText, span) return Some(FSharpDocumentSpan(doc, span)) - | None -> return None + | ValueNone -> return None } } |> CancellableTask.whenAll @@ -122,14 +122,14 @@ module FSharpFindUsagesService = let! declarationSpans = match declarationRange with - | Some range -> cancellableTask { return! rangeToDocumentSpans (document.Project.Solution, range) } + | Some range -> rangeToDocumentSpans (document.Project.Solution, range) | None -> CancellableTask.singleton [||] let declarationSpans = declarationSpans |> Array.distinctBy (fun x -> x.Document.FilePath, x.Document.Project.FilePath) - let isExternal = declarationSpans |> Array.isEmpty + let isExternal = Array.isEmpty declarationSpans let displayParts = ImmutableArray.Create(Microsoft.CodeAnalysis.TaggedText(TextTags.Text, symbol.Ident.idText)) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 1199b14477d..3ad1a4c9c48 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -17,7 +17,6 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation open Microsoft.VisualStudio open Microsoft.VisualStudio.Shell -open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.LanguageServices open FSharp.Compiler.CodeAnalysis @@ -30,6 +29,7 @@ open System.Text.RegularExpressions open CancellableTasks open Microsoft.VisualStudio.FSharp.Editor.Telemetry open Microsoft.VisualStudio.Telemetry +open System.Collections.Generic module private Symbol = let fullName (root: ISymbol) : string = @@ -110,45 +110,6 @@ module private ExternalSymbol = | _ -> [] -// TODO: Uncomment code when VS has a fix for updating the status bar. -type StatusBar() = - let statusBar = - ServiceProvider.GlobalProvider.GetService() - - let mutable _searchIcon = - int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj - - let _clear () = - // unfreeze the statusbar - statusBar.FreezeOutput 0 |> ignore - statusBar.Clear() |> ignore - - member _.Message(_msg: string) = () - //let _, frozen = statusBar.IsFrozen() - //// unfreeze the status bar - //if frozen <> 0 then statusBar.FreezeOutput 0 |> ignore - //statusBar.SetText msg |> ignore - //// freeze the status bar - //statusBar.FreezeOutput 1 |> ignore - - member this.TempMessage(_msg: string) = () - //this.Message msg - //async { - // do! Async.Sleep 4000 - // match statusBar.GetText() with - // | 0, currentText when currentText <> msg -> () - // | _ -> clear() - //}|> Async.Start - - member _.Clear() = () //clear() - - /// Animated magnifying glass that displays on the status bar while a symbol search is in progress. - member _.Animate() : IDisposable = - //statusBar.Animation (1, &searchIcon) |> ignore - { new IDisposable with - member _.Dispose() = () - } //statusBar.Animation(0, &searchIcon) |> ignore } - type internal FSharpGoToDefinitionNavigableItem(document, sourceSpan) = inherit FSharpNavigableItem(Glyph.BasicFile, ImmutableArray.Empty, document, sourceSpan) @@ -158,10 +119,65 @@ type internal FSharpGoToDefinitionResult = | ExternalAssembly of FSharpSymbolUse * MetadataReference seq type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = + + let rec areTypesEqual (ty1: FSharpType) (ty2: FSharpType) = + let ty1 = ty1.StripAbbreviations() + let ty2 = ty2.StripAbbreviations() + + let generic = + ty1.IsGenericParameter && ty2.IsGenericParameter + || (ty1.GenericArguments.Count = ty2.GenericArguments.Count + && (ty1.GenericArguments, ty2.GenericArguments) ||> Seq.forall2 areTypesEqual) + + if generic then + true + else + let namesEqual = ty1.TypeDefinition.DisplayName = ty2.TypeDefinition.DisplayName + let accessPathsEqual = ty1.TypeDefinition.AccessPath = ty2.TypeDefinition.AccessPath + namesEqual && accessPathsEqual + + let tryFindExternalSymbolUse (targetSymbolUse: FSharpSymbolUse) (x: FSharpSymbolUse) = + match x.Symbol, targetSymbolUse.Symbol with + | (:? FSharpEntity as symbol1), (:? FSharpEntity as symbol2) when x.IsFromDefinition -> symbol1.DisplayName = symbol2.DisplayName + + | (:? FSharpMemberOrFunctionOrValue as symbol1), (:? FSharpMemberOrFunctionOrValue as symbol2) -> + symbol1.DisplayName = symbol2.DisplayName + && (match symbol1.DeclaringEntity, symbol2.DeclaringEntity with + | Some e1, Some e2 -> e1.CompiledName = e2.CompiledName + | _ -> false) + && symbol1.GenericParameters.Count = symbol2.GenericParameters.Count + && symbol1.CurriedParameterGroups.Count = symbol2.CurriedParameterGroups.Count + && ((symbol1.CurriedParameterGroups, symbol2.CurriedParameterGroups) + ||> Seq.forall2 (fun pg1 pg2 -> + let pg1, pg2 = pg1.ToArray(), pg2.ToArray() + // We filter out/fixup first "unit" parameter in the group, since it just represents the `()` call notation, for example `"string".Clone()` will have one curried group with one parameter which type is unit. + let pg1 = // If parameter has no name and it's unit type, filter it out + if + pg1.Length > 0 + && Option.isNone pg1[0].Name + && pg1[0].Type.StripAbbreviations().TypeDefinition.DisplayName = "Unit" + then + pg1[1..] + else + pg1 + + pg1.Length = pg2.Length + && ((pg1, pg2) ||> Seq.forall2 (fun p1 p2 -> areTypesEqual p1.Type p2.Type)))) + && areTypesEqual symbol1.ReturnParameter.Type symbol2.ReturnParameter.Type + | (:? FSharpField as symbol1), (:? FSharpField as symbol2) when x.IsFromDefinition -> + symbol1.DisplayName = symbol2.DisplayName + && (match symbol1.DeclaringEntity, symbol2.DeclaringEntity with + | Some e1, Some e2 -> e1.CompiledName = e2.CompiledName + | _ -> false) + | (:? FSharpUnionCase as symbol1), (:? FSharpUnionCase as symbol2) -> + symbol1.DisplayName = symbol2.DisplayName + && symbol1.DeclaringEntity.CompiledName = symbol2.DeclaringEntity.CompiledName + | _ -> false + /// Use an origin document to provide the solution & workspace used to /// find the corresponding textSpan and INavigableItem for the range let rangeToNavigableItem (range: range, document: Document) = - async { + cancellableTask { let fileName = try System.IO.Path.GetFullPath range.FileName @@ -177,62 +193,86 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask match RoslynHelpers.TryFSharpRangeToTextSpan(refSourceText, range) with - | None -> return None - | Some refTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(refDocument, refTextSpan)) + | ValueNone -> return None + | ValueSome refTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(refDocument, refTextSpan)) else return None } /// Helper function that is used to determine the navigation strategy to apply, can be tuned towards signatures or implementation files. member private _.FindSymbolHelper(originDocument: Document, originRange: range, sourceText: SourceText, preferSignature: bool) = - asyncMaybe { - let userOpName = "FindSymbolHelper" - let! originTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, originRange) - let position = originTextSpan.Start - let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) - let textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - let idRange = lexerSymbol.Ident.idRange + let originTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, originRange) - let! ct = Async.CancellationToken |> liftAsync + match originTextSpan with + | ValueNone -> CancellableTask.singleton None + | ValueSome originTextSpan -> + cancellableTask { + let userOpName = "FindSymbolHelper" - let! _, checkFileResults = - originDocument.GetFSharpParseAndCheckResultsAsync(nameof (GoToDefinition)) - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync + let position = originTextSpan.Start + let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) - let! fsSymbolUse = - checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + match lexerSymbol with + | None -> return None + | Some lexerSymbol -> + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let idRange = lexerSymbol.Ident.idRange - let symbol = fsSymbolUse.Symbol - // if the tooltip was spawned in an implementation file and we have a range targeting - // a signature file, try to find the corresponding implementation file and target the - // desired symbol - if isSignatureFile fsSymbolUse.FileName && preferSignature = false then - let fsfilePath = Path.ChangeExtension(originRange.FileName, "fs") + let! ct = CancellableTask.getCancellationToken () - if not (File.Exists fsfilePath) then - return! None - else - let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath - let! implSourceText = implDoc.GetTextAsync() - - let! _, checkFileResults = - implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync - - let symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol - let! implSymbol = symbolUses |> Array.tryHead - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, implSymbol.Range) - return FSharpGoToDefinitionNavigableItem(implDoc, implTextSpan) - else - let! targetDocument = originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.Range - return! rangeToNavigableItem (fsSymbolUse.Range, targetDocument) - } + let! _, checkFileResults = originDocument.GetFSharpParseAndCheckResultsAsync(nameof (GoToDefinition)) + + let fsSymbolUse = + checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + + match fsSymbolUse with + | None -> return None + | Some fsSymbolUse -> + + let symbol = fsSymbolUse.Symbol + // if the tooltip was spawned in an implementation file and we have a range targeting + // a signature file, try to find the corresponding implementation file and target the + // desired symbol + if isSignatureFile fsSymbolUse.FileName && preferSignature = false then + let fsfilePath = Path.ChangeExtension(originRange.FileName, "fs") + + if not (File.Exists fsfilePath) then + return None + else + let implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath + + match implDoc with + | ValueNone -> return None + | ValueSome implDoc -> + let! implSourceText = implDoc.GetTextAsync(ct) + + let! _, checkFileResults = implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) + + let symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbol, ct) + + let implSymbol = Array.tryHeadV symbolUses + + match implSymbol with + | ValueNone -> return None + | ValueSome implSymbol -> + let implTextSpan = + RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, implSymbol.Range) + + match implTextSpan with + | ValueNone -> return None + | ValueSome implTextSpan -> return Some(FSharpGoToDefinitionNavigableItem(implDoc, implTextSpan)) + else + let targetDocument = + originDocument.Project.Solution.TryGetDocumentFromFSharpRange fsSymbolUse.Range + + match targetDocument with + | None -> return None + | Some targetDocument -> + let! navItem = rangeToNavigableItem (fsSymbolUse.Range, targetDocument) + return navItem + } /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first /// instance of its presence in the provided source file. The first case is needed to return proper declaration location for @@ -261,9 +301,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return implSymbol.Range } - member private this.FindDefinitionAtPosition(originDocument: Document, position: int, cancellationToken: CancellationToken) = - asyncMaybe { + member internal this.FindDefinitionAtPosition(originDocument: Document, position: int) = + cancellableTask { let userOpName = "FindDefinitionAtPosition" + let! cancellationToken = CancellableTask.getCancellationToken () let! sourceText = originDocument.GetTextAsync(cancellationToken) let textLine = sourceText.Lines.GetLineFromPosition position let textLinePos = sourceText.Lines.GetLinePosition position @@ -271,131 +312,196 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - let! ct = Async.CancellationToken |> liftAsync + let! cancellationToken = CancellableTask.getCancellationToken () let preferSignature = isSignatureFile originDocument.FilePath let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) - let idRange = lexerSymbol.Ident.idRange - - let! _, checkFileResults = - originDocument.GetFSharpParseAndCheckResultsAsync(userOpName) - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync - - let declarations = - checkFileResults.GetDeclarationLocation( - fcsTextLineNumber, - lexerSymbol.Ident.idRange.EndColumn, - textLineString, - lexerSymbol.FullIsland, - preferSignature - ) - let! targetSymbolUse = - checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) - - match declarations with - | FindDeclResult.ExternalDecl (assembly, targetExternalSym) -> - let projectOpt = - originDocument.Project.Solution.Projects - |> Seq.tryFindV (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase)) - - match projectOpt with - | ValueSome project -> - let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, (fun _ -> true)) - - let roslynSymbols = - symbols |> Seq.collect ExternalSymbol.ofRoslynSymbol |> Array.ofSeq - - let! symbol = - roslynSymbols - |> Seq.tryPick (fun (sym, externalSym) -> if externalSym = targetExternalSym then Some sym else None) - - let! location = symbol.Locations |> Seq.tryHead - - return - (FSharpGoToDefinitionResult.NavigableItem( - FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan) - ), - idRange) - | _ -> - let metadataReferences = originDocument.Project.MetadataReferences - return (FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) - - | FindDeclResult.DeclFound targetRange -> - // If the file is not associated with a document, it's considered external. - if not (originDocument.Project.Solution.ContainsDocumentWithFilePath(targetRange.FileName)) then - let metadataReferences = originDocument.Project.MetadataReferences - return (FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) - else - // if goto definition is called at we are alread at the declaration location of a symbol in - // either a signature or an implementation file then we jump to it's respective postion in thethe - if - lexerSymbol.Range = targetRange - then - // jump from signature to the corresponding implementation - if isSignatureFile originDocument.FilePath then - let implFilePath = Path.ChangeExtension(originDocument.FilePath, "fs") - - if not (File.Exists implFilePath) then - return! None - else - let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) - let! implSourceText = implDocument.GetTextAsync(cancellationToken) |> liftTaskAsync - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) - let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) - return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - else // jump from implementation to the corresponding signature - let declarations = - checkFileResults.GetDeclarationLocation( - fcsTextLineNumber, - lexerSymbol.Ident.idRange.EndColumn, - textLineString, - lexerSymbol.FullIsland, - true - ) - - match declarations with - | FindDeclResult.DeclFound targetRange -> - let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName - let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync - let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) - let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) - return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - | _ -> return! None - // when the target range is different follow the navigation convention of - // - gotoDefn origin = signature , gotoDefn destination = signature - // - gotoDefn origin = implementation, gotoDefn destination = implementation - else - let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName - let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync - let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) - // if the gotodef call originated from a signature and the returned target is a signature, navigate there - if isSignatureFile targetRange.FileName && preferSignature then - let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) - return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - else // we need to get an FSharpSymbol from the targetRange found in the signature - // that symbol will be used to find the destination in the corresponding implementation file - let implFilePath = - // Bugfix: apparently sigDocument not always is a signature file - if isSignatureFile sigDocument.FilePath then - Path.ChangeExtension(sigDocument.FilePath, "fs") - else - sigDocument.FilePath + match lexerSymbol with + | None -> return ValueNone + | Some lexerSymbol -> + + let idRange = lexerSymbol.Ident.idRange - let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + let! _, checkFileResults = originDocument.GetFSharpParseAndCheckResultsAsync(userOpName) - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + let declarations = + checkFileResults.GetDeclarationLocation( + fcsTextLineNumber, + idRange.EndColumn, + textLineString, + lexerSymbol.FullIsland, + preferSignature + ) - let! implSourceText = implDocument.GetTextAsync() |> liftTaskAsync - let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) - let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) - return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) - | _ -> return! None + let targetSymbolUse = + checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) + + match targetSymbolUse with + | None -> return ValueNone + | Some targetSymbolUse -> + + match declarations with + | FindDeclResult.ExternalDecl (assembly, targetExternalSym) -> + let projectOpt = + originDocument.Project.Solution.Projects + |> Seq.tryFindV (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase)) + + match projectOpt with + | ValueSome project -> + let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, (fun _ -> true), cancellationToken) + + let roslynSymbols = Seq.collect ExternalSymbol.ofRoslynSymbol symbols + + let symbol = + Seq.tryPickV + (fun (sym, externalSym) -> + if externalSym = targetExternalSym then + ValueSome sym + else + ValueNone) + roslynSymbols + + let location = + symbol + |> ValueOption.map (fun s -> s.Locations) + |> ValueOption.bind Seq.tryHeadV + + match location with + | ValueNone -> return ValueNone + | ValueSome location -> + + return + ValueSome( + FSharpGoToDefinitionResult.NavigableItem( + FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan) + ), + idRange + ) + | _ -> + let metadataReferences = originDocument.Project.MetadataReferences + return ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) + + | FindDeclResult.DeclFound targetRange -> + // If the file is not associated with a document, it's considered external. + if not (originDocument.Project.Solution.ContainsDocumentWithFilePath(targetRange.FileName)) then + let metadataReferences = originDocument.Project.MetadataReferences + return ValueSome(FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) + else + // if goto definition is called at we are alread at the declaration location of a symbol in + // either a signature or an implementation file then we jump to it's respective postion in thethe + if + lexerSymbol.Range = targetRange + then + // jump from signature to the corresponding implementation + if isSignatureFile originDocument.FilePath then + let implFilePath = Path.ChangeExtension(originDocument.FilePath, "fs") + + if not (File.Exists implFilePath) then + return ValueNone + else + let implDocument = + originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + + match implDocument with + | ValueNone -> return ValueNone + | ValueSome implDocument -> + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + + match targetRange with + | None -> return ValueNone + | Some targetRange -> + let! implSourceText = implDocument.GetTextAsync(cancellationToken) + + let implTextSpan = + RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) + + match implTextSpan with + | ValueNone -> return ValueNone + | ValueSome implTextSpan -> + let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) + return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) + + else // jump from implementation to the corresponding signature + let declarations = + checkFileResults.GetDeclarationLocation( + fcsTextLineNumber, + idRange.EndColumn, + textLineString, + lexerSymbol.FullIsland, + true + ) + + match declarations with + | FindDeclResult.DeclFound targetRange -> + let sigDocument = + originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + + match sigDocument with + | ValueNone -> return ValueNone + | ValueSome sigDocument -> + let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) + + let sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) + + match sigTextSpan with + | ValueNone -> return ValueNone + | ValueSome sigTextSpan -> + let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) + return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) + | _ -> return ValueNone + // when the target range is different follow the navigation convention of + // - gotoDefn origin = signature , gotoDefn destination = signature + // - gotoDefn origin = implementation, gotoDefn destination = implementation + else + let sigDocument = + originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName + + match sigDocument with + | ValueNone -> return ValueNone + | ValueSome sigDocument -> + let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) + let sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sigSourceText, targetRange) + + match sigTextSpan with + | ValueNone -> return ValueNone + | ValueSome sigTextSpan -> + // if the gotodef call originated from a signature and the returned target is a signature, navigate there + if isSignatureFile targetRange.FileName && preferSignature then + let navItem = FSharpGoToDefinitionNavigableItem(sigDocument, sigTextSpan) + return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) + else // we need to get an FSharpSymbol from the targetRange found in the signature + // that symbol will be used to find the destination in the corresponding implementation file + let implFilePath = + // Bugfix: apparently sigDocument not always is a signature file + if isSignatureFile sigDocument.FilePath then + Path.ChangeExtension(sigDocument.FilePath, "fs") + else + sigDocument.FilePath + + let implDocument = + originDocument.Project.Solution.TryGetDocumentFromPath implFilePath + + match implDocument with + | ValueNone -> return ValueNone + | ValueSome implDocument -> + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + + match targetRange with + | None -> return ValueNone + | Some targetRange -> + let! implSourceText = implDocument.GetTextAsync(cancellationToken) + + let implTextSpan = + RoslynHelpers.TryFSharpRangeToTextSpan(implSourceText, targetRange) + + match implTextSpan with + | ValueNone -> return ValueNone + | ValueSome implTextSpan -> + let navItem = FSharpGoToDefinitionNavigableItem(implDocument, implTextSpan) + return ValueSome(FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) + | _ -> return ValueNone } /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition @@ -406,42 +512,24 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = member this.FindDefinitionOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSourceText: SourceText) = this.FindSymbolHelper(targetDocument, symbolRange, targetSourceText, preferSignature = false) - member this.FindDefinitionsForPeekTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = - this.FindDefinitionAtPosition(originDocument, position, cancellationToken) - |> Async.map (Option.toArray >> Array.toSeq) - |> RoslynHelpers.StartAsyncAsTask cancellationToken - /// 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.FindDefinitionTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = - this.FindDefinitionAtPosition(originDocument, position, cancellationToken) - |> RoslynHelpers.StartAsyncAsTask cancellationToken + member this.FindDefinitionAsync(originDocument: Document, position: int) = + this.FindDefinitionAtPosition(originDocument, position) /// 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 _.TryNavigateToTextSpan - ( - document: Document, - textSpan: Microsoft.CodeAnalysis.Text.TextSpan, - cancellationToken: CancellationToken - ) = + member _.TryNavigateToTextSpan(document: Document, textSpan: TextSpan, cancellationToken: CancellationToken) = let navigableItem = FSharpGoToDefinitionNavigableItem(document, textSpan) let workspace = document.Project.Solution.Workspace let navigationService = workspace.Services.GetService() - let navigationSucceeded = - navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - - if not navigationSucceeded then - StatusBar().TempMessage(SR.CannotNavigateUnknown()) + navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) + |> ignore member _.NavigateToItem(navigableItem: FSharpNavigableItem, cancellationToken: CancellationToken) = - let statusBar = StatusBar() - use __ = statusBar.Animate() - - statusBar.Message(SR.NavigatingTo()) let workspace = navigableItem.Document.Project.Solution.Workspace @@ -452,46 +540,38 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let result = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - if result then - statusBar.Clear() - else - statusBar.TempMessage(SR.CannotNavigateUnknown()) + result /// 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, - cancellationToken: CancellationToken - ) = - asyncMaybe { + member this.NavigateToSymbolDeclarationAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range) = + cancellableTask { let! item = this.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) - return this.NavigateToItem(item, cancellationToken) + + match item with + | None -> return false + | Some item -> + let! cancellationToken = CancellableTask.getCancellationToken () + return this.NavigateToItem(item, cancellationToken) } /// Find the definition location (implementation file/.fs) of the target symbol - member this.NavigateToSymbolDefinitionAsync - ( - targetDocument: Document, - targetSourceText: SourceText, - symbolRange: range, - cancellationToken: CancellationToken - ) = - asyncMaybe { + member this.NavigateToSymbolDefinitionAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range) = + cancellableTask { let! item = this.FindDefinitionOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) - return this.NavigateToItem(item, cancellationToken) + + match item with + | None -> return false + | Some item -> + let! cancellationToken = CancellableTask.getCancellationToken () + return this.NavigateToItem(item, cancellationToken) } member this.NavigateToExternalDeclaration ( targetSymbolUse: FSharpSymbolUse, metadataReferences: seq, - cancellationToken: CancellationToken, - statusBar: StatusBar + cancellationToken: CancellationToken ) = - use __ = statusBar.Animate() - statusBar.Message(SR.NavigatingTo()) let textOpt = match targetSymbolUse.Symbol with @@ -513,104 +593,55 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = |> Option.map (fun text -> text, symbol.DisplayName) | _ -> None - let result = - match textOpt with - | Some (text, fileName) -> - let tmpProjInfo, tmpDocInfo = - MetadataAsSource.generateTemporaryDocument ( - AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), - fileName, - metadataReferences - ) + match textOpt with + | Some (text, fileName) -> + let tmpProjInfo, tmpDocInfo = + MetadataAsSource.generateTemporaryDocument ( + AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), + fileName, + metadataReferences + ) - let tmpShownDocOpt = - metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) + let tmpShownDocOpt = + metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) - match tmpShownDocOpt with - | ValueSome tmpShownDoc -> - let goToAsync = - asyncMaybe { + match tmpShownDocOpt with + | ValueSome tmpShownDoc -> + let goToAsync = + cancellableTask { - let! ct = Async.CancellationToken |> liftAsync + let! cancellationToken = CancellableTask.getCancellationToken () - let! _, checkResults = - tmpShownDoc.GetFSharpParseAndCheckResultsAsync("NavigateToExternalDeclaration") - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync + let! _, checkResults = tmpShownDoc.GetFSharpParseAndCheckResultsAsync("NavigateToExternalDeclaration") - let! r = - let rec areTypesEqual (ty1: FSharpType) (ty2: FSharpType) = - let ty1 = ty1.StripAbbreviations() - let ty2 = ty2.StripAbbreviations() + let r = + // This tries to find the best possible location of the target symbol's location in the metadata source. + // We really should rely on symbol equality within FCS instead of doing it here, + // but the generated metadata as source isn't perfect for symbol equality. + let symbols = checkResults.GetAllUsesOfAllSymbolsInFile(cancellationToken) - let generic = - ty1.IsGenericParameter && ty2.IsGenericParameter - || (ty1.GenericArguments.Count = ty2.GenericArguments.Count - && (ty1.GenericArguments, ty2.GenericArguments) ||> Seq.forall2 areTypesEqual) + symbols + |> Seq.tryFindV (tryFindExternalSymbolUse targetSymbolUse) + |> ValueOption.map (fun x -> x.Range) + |> ValueOption.toOption - if generic then - true - else - let namesEqual = ty1.TypeDefinition.DisplayName = ty2.TypeDefinition.DisplayName - let accessPathsEqual = ty1.TypeDefinition.AccessPath = ty2.TypeDefinition.AccessPath - namesEqual && accessPathsEqual - - // This tries to find the best possible location of the target symbol's location in the metadata source. - // We really should rely on symbol equality within FCS instead of doing it here, - // but the generated metadata as source isn't perfect for symbol equality. - checkResults.GetAllUsesOfAllSymbolsInFile(cancellationToken) - |> Seq.tryFindV (fun x -> - match x.Symbol, targetSymbolUse.Symbol with - | (:? FSharpEntity as symbol1), (:? FSharpEntity as symbol2) when x.IsFromDefinition -> - symbol1.DisplayName = symbol2.DisplayName - | (:? FSharpMemberOrFunctionOrValue as symbol1), (:? FSharpMemberOrFunctionOrValue as symbol2) -> - symbol1.DisplayName = symbol2.DisplayName - && (match symbol1.DeclaringEntity, symbol2.DeclaringEntity with - | Some e1, Some e2 -> e1.CompiledName = e2.CompiledName - | _ -> false) - && symbol1.GenericParameters.Count = symbol2.GenericParameters.Count - && symbol1.CurriedParameterGroups.Count = symbol2.CurriedParameterGroups.Count - && ((symbol1.CurriedParameterGroups, symbol2.CurriedParameterGroups) - ||> Seq.forall2 (fun pg1 pg2 -> - pg1.Count = pg2.Count - && ((pg1, pg2) ||> Seq.forall2 (fun p1 p2 -> areTypesEqual p1.Type p2.Type)))) - && areTypesEqual symbol1.ReturnParameter.Type symbol2.ReturnParameter.Type - | (:? FSharpField as symbol1), (:? FSharpField as symbol2) when x.IsFromDefinition -> - symbol1.DisplayName = symbol2.DisplayName - && (match symbol1.DeclaringEntity, symbol2.DeclaringEntity with - | Some e1, Some e2 -> e1.CompiledName = e2.CompiledName - | _ -> false) - | (:? FSharpUnionCase as symbol1), (:? FSharpUnionCase as symbol2) -> - symbol1.DisplayName = symbol2.DisplayName - && symbol1.DeclaringEntity.CompiledName = symbol2.DeclaringEntity.CompiledName - | _ -> false) - |> ValueOption.map (fun x -> x.Range) - |> ValueOption.toOption - - let span = - match RoslynHelpers.TryFSharpRangeToTextSpan(tmpShownDoc.GetTextAsync(cancellationToken).Result, r) with - | Some span -> span - | _ -> TextSpan() - - return span - } - - let span = - match Async.RunImmediateExceptOnUI(goToAsync, cancellationToken = cancellationToken) with - | Some span -> span - | _ -> TextSpan() - - let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) - this.NavigateToItem(navItem, cancellationToken) - true - | _ -> false - | _ -> false + match r with + | None -> return TextSpan.empty + | Some r -> + let! text = tmpShownDoc.GetTextAsync(cancellationToken) - if result then - statusBar.Clear() - else - statusBar.TempMessage(SR.CannotNavigateUnknown()) + match RoslynHelpers.TryFSharpRangeToTextSpan(text, r) with + | ValueSome span -> return span + | _ -> return TextSpan.empty + + } + + let span = CancellableTask.runSynchronously cancellationToken goToAsync + + let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) + this.NavigateToItem(navItem, cancellationToken) + | _ -> false + | _ -> false type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, initialDoc: Document, thisSymbolUseRange: range) = @@ -628,33 +659,40 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, ThreadHelper.JoinableTaskFactory.Run( SR.NavigatingTo(), (fun _progress cancellationToken -> - Async.StartImmediateAsTask( - asyncMaybe { - let! targetDoc = solution.TryGetDocumentFromFSharpRange(range, initialDoc.Project.Id) + cancellableTask { + let targetDoc = solution.TryGetDocumentFromFSharpRange(range, initialDoc.Project.Id) + + match targetDoc with + | None -> () + | Some targetDoc -> + + let! cancellationToken = CancellableTask.getCancellationToken () + let! targetSource = targetDoc.GetTextAsync(cancellationToken) - let! targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(targetSource, range) - let gtd = GoToDefinition(metadataAsSource) - - // Whenever possible: - // - signature files (.fsi) should navigate to other signature files - // - implementation files (.fs) should navigate to other implementation files - if isSignatureFile initialDoc.FilePath then - // Target range will point to .fsi file if only there is one so we can just use Roslyn navigation service. - return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) - else - // Navigation request was made in a .fs file, so we try to find the implmentation of the symbol at target range. - // This is the part that may take some time, because of type checks involved. - let! result = - gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, cancellationToken) - |> liftAsync - - if result.IsNone then - // In case the above fails, we just navigate to target range. - return gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) - } - |> Async.Ignore, - cancellationToken - )), + let targetTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan(targetSource, range) + + match targetTextSpan with + | ValueNone -> () + | ValueSome targetTextSpan -> + + let gtd = GoToDefinition(metadataAsSource) + + // Whenever possible: + // - signature files (.fsi) should navigate to other signature files + // - implementation files (.fs) should navigate to other implementation files + if isSignatureFile initialDoc.FilePath then + // Target range will point to .fsi file if only there is one so we can just use Roslyn navigation service. + do gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) + else + // Navigation request was made in a .fs file, so we try to find the implmentation of the symbol at target range. + // This is the part that may take some time, because of type checks involved. + let! result = gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range) + + if not result then + // In case the above fails, we just navigate to target range. + do gtd.TryNavigateToTextSpan(targetDoc, targetTextSpan, cancellationToken) + } + |> CancellableTask.start cancellationToken), // Default wait time before VS shows the dialog allowing to cancel the long running task is 2 seconds. // This seems a bit too long to leave the user without any feedback, so we shorten it to 1 second. // Note: it seems anything less than 1 second will get rounded down to zero, resulting in flashing dialog @@ -664,52 +702,46 @@ type internal FSharpNavigation(metadataAsSource: FSharpMetadataAsSourceService, with :? OperationCanceledException -> () - member _.FindDefinitions(position, cancellationToken) = - let gtd = GoToDefinition(metadataAsSource) - let task = gtd.FindDefinitionsForPeekTask(initialDoc, position, cancellationToken) - task.Wait(cancellationToken) - let results = task.Result + member _.FindDefinitionsAsync(position) = + cancellableTask { + let gtd = GoToDefinition(metadataAsSource) + let! result = gtd.FindDefinitionAtPosition(initialDoc, position) - results - |> Seq.choose (fun (result, _) -> - match result with - | FSharpGoToDefinitionResult.NavigableItem (navItem) -> Some navItem - | _ -> None) + return + match result with + | ValueSome (FSharpGoToDefinitionResult.NavigableItem (navItem), _) -> ImmutableArray.create navItem + | _ -> ImmutableArray.empty + } member _.TryGoToDefinition(position, cancellationToken) = - let gtd = GoToDefinition(metadataAsSource) - let statusBar = StatusBar() - let gtdTask = gtd.FindDefinitionTask(initialDoc, position, cancellationToken) - + // Once we migrate to Roslyn-exposed MAAS and sourcelink (https://github.com/dotnet/fsharp/issues/13951), this can be a "normal" task // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try - // This call to Wait() is fine because we want to be able to provide the error message in the status bar. use _ = TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.GoToDefinition, [||]) - gtdTask.Wait(cancellationToken) + let gtd = GoToDefinition(metadataAsSource) + let gtdTask = gtd.FindDefinitionAsync (initialDoc, position) cancellationToken + + gtdTask.Wait() if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then - match gtdTask.Result.Value with - | FSharpGoToDefinitionResult.NavigableItem (navItem), _ -> - gtd.NavigateToItem(navItem, cancellationToken) - // 'true' means do it, like Sheev Palpatine would want us to. + match gtdTask.Result with + | ValueSome (FSharpGoToDefinitionResult.NavigableItem (navItem), _) -> + gtd.NavigateToItem(navItem, cancellationToken) |> ignore true - | FSharpGoToDefinitionResult.ExternalAssembly (targetSymbolUse, metadataReferences), _ -> - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar) - // 'true' means do it, like Sheev Palpatine would want us to. + | ValueSome (FSharpGoToDefinitionResult.ExternalAssembly (targetSymbolUse, metadataReferences), _) -> + gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken) + |> ignore + true + | _ -> false else - statusBar.TempMessage(SR.CannotDetermineSymbol()) false with exc -> TelemetryReporter.ReportFault(TelemetryEvents.GoToDefinition, FaultSeverity.General, exc) - statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) - - // Don't show the dialog box as it's most likely that the user cancelled. - // Don't make them click twice. - true + false [] type internal SymbolMemberType = @@ -744,24 +776,31 @@ type internal DocCommentId = type FSharpNavigableLocation(metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = interface IFSharpNavigableLocation with member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = - asyncMaybe { + cancellableTask { let targetPath = symbolRange.FileName - let! targetDoc = project.Solution.TryGetDocumentFromFSharpRange(symbolRange, project.Id) - let! targetSource = targetDoc.GetTextAsync(cancellationToken) - let gtd = GoToDefinition(metadataAsSource) - - let (|Signature|Implementation|) filepath = - if isSignatureFile filepath then - Signature - else - Implementation - - match targetPath with - | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, cancellationToken) - | Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange, cancellationToken) + + let! cancellationToken = CancellableTask.getCancellationToken () + + let targetDoc = + project.Solution.TryGetDocumentFromFSharpRange(symbolRange, project.Id) + + match targetDoc with + | None -> return false + | Some targetDoc -> + let! targetSource = targetDoc.GetTextAsync(cancellationToken) + let gtd = GoToDefinition(metadataAsSource) + + let (|Signature|Implementation|) filepath = + if isSignatureFile filepath then + Signature + else + Implementation + + match targetPath with + | Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange) + | Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange) } - |> Async.map Option.isSome - |> RoslynHelpers.StartAsyncAsTask cancellationToken + |> CancellableTask.start cancellationToken [)>] [)>] diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index a3a800c9ef6..3130163b192 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -4,12 +4,13 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition open System.Threading -open System.Threading.Tasks open FSharp.Compiler.Text.Range open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor +open CancellableTasks +open System.Collections.Generic [)>] [)>] @@ -18,17 +19,15 @@ type internal FSharpGoToDefinitionService [] (metadataAsSo interface IFSharpGoToDefinitionService with /// Invoked with Peek Definition. member _.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) = - let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) - - navigation.FindDefinitions(position, cancellationToken) |> Task.FromResult + cancellableTask { + let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) + let! res = navigation.FindDefinitionsAsync(position) + return (res :> IEnumerable<_>) + } + |> CancellableTask.start cancellationToken /// Invoked with Go to Definition. /// Try to navigate to the definiton of the symbol at the symbolRange in the originDocument member _.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) = - let statusBar = StatusBar() - statusBar.Message(SR.LocatingSymbol()) - use __ = statusBar.Animate() - let navigation = FSharpNavigation(metadataAsSource, document, rangeStartup) - navigation.TryGoToDefinition(position, cancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs index ab7a4b6a29a..6d918913fc5 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigableSymbolsService.fs @@ -16,12 +16,14 @@ open Microsoft.VisualStudio.Text.Editor open Microsoft.VisualStudio.Utilities open Microsoft.VisualStudio.FSharp.Editor.Telemetry open Microsoft.VisualStudio.Telemetry +open CancellableTasks.CancellableTaskBuilder +open CancellableTasks [] type internal FSharpNavigableSymbol(item: FSharpNavigableItem, span: SnapshotSpan, gtd: GoToDefinition) = interface INavigableSymbol with member _.Navigate(_: INavigableRelationship) = - gtd.NavigateToItem(item, CancellationToken.None) + gtd.NavigateToItem(item, CancellationToken.None) |> ignore member _.Relationships = seq { yield PredefinedNavigableRelationships.Definition } @@ -31,7 +33,6 @@ type internal FSharpNavigableSymbolSource(metadataAsSource) = let mutable disposed = false let gtd = GoToDefinition(metadataAsSource) - let statusBar = StatusBar() interface INavigableSymbolSource with member _.GetNavigableSymbolAsync(triggerSpan: SnapshotSpan, cancellationToken: CancellationToken) = @@ -39,29 +40,22 @@ type internal FSharpNavigableSymbolSource(metadataAsSource) = if disposed then null else - asyncMaybe { + cancellableTask { + use _ = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.GoToDefinitionGetSymbol, [||]) + let snapshot = triggerSpan.Snapshot let position = triggerSpan.Start.Position let document = snapshot.GetOpenDocumentInCurrentContextWithChanges() - let! sourceText = document.GetTextAsync(cancellationToken) |> liftTaskAsync - - statusBar.Message(SR.LocatingSymbol()) - use _ = statusBar.Animate() + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync(cancellationToken) - let gtdTask = gtd.FindDefinitionTask(document, position, cancellationToken) - - // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. - // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try - // This call to Wait() is fine because we want to be able to provide the error message in the status bar. - use _ = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.GoToDefinitionGetSymbol, [||]) - - gtdTask.Wait(cancellationToken) - statusBar.Clear() + let! definition = gtd.FindDefinitionAsync(document, position) - if gtdTask.Status = TaskStatus.RanToCompletion && gtdTask.Result.IsSome then - let result, range = gtdTask.Result.Value + match definition with + | ValueNone -> return null + | ValueSome (result, range) -> let declarationTextSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, range) let declarationSpan = Span(declarationTextSpan.Start, declarationTextSpan.Length) @@ -78,8 +72,12 @@ type internal FSharpNavigableSymbolSource(metadataAsSource) = // Need to new up a CTS here instead of re-using the other one, since VS // will navigate disconnected from the outer routine, leading to an // OperationCancelledException if you use the one defined outside. + // TODO: review if it's a proper way of doing it use ct = new CancellationTokenSource() - gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, ct.Token, statusBar) + + do + gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, ct.Token) + |> ignore member _.Relationships = seq { yield PredefinedNavigableRelationships.Definition } @@ -87,21 +85,13 @@ type internal FSharpNavigableSymbolSource(metadataAsSource) = } return nav - else - statusBar.TempMessage(SR.CannotDetermineSymbol()) - // The NavigableSymbols API accepts 'null' when there's nothing to navigate to. - return null with exc -> TelemetryReporter.ReportFault(TelemetryEvents.GoToDefinitionGetSymbol, FaultSeverity.General, exc) - - statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) - // The NavigableSymbols API accepts 'null' when there's nothing to navigate to. return null } - |> Async.map Option.toObj - |> RoslynHelpers.StartAsyncAsTask cancellationToken + |> CancellableTask.start cancellationToken member _.Dispose() = disposed <- true diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 1107e6f5869..8cce297ac0d 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -44,7 +44,7 @@ type internal FSharpNavigateToSearchService [] | true, (version, items) when version = currentVersion -> return items | _ -> let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigateToSearchService)) - let items = parseResults.ParseTree |> NavigateTo.GetNavigableItems + let items = NavigateTo.GetNavigableItems parseResults.ParseTree cache[document.Id] <- currentVersion, items return items } @@ -135,46 +135,58 @@ type internal FSharpNavigateToSearchService [] if item.NeedsBackticks then match name.IndexOf(searchPattern, StringComparison.CurrentCultureIgnoreCase) with - | i when i > 0 -> PatternMatch(PatternMatchKind.Substring, false, false) |> Some - | 0 when name.Length = searchPattern.Length -> PatternMatch(PatternMatchKind.Exact, false, false) |> Some - | 0 -> PatternMatch(PatternMatchKind.Prefix, false, false) |> Some - | _ -> None + | i when i > 0 -> ValueSome(PatternMatch(PatternMatchKind.Substring, false, false)) + | 0 when name.Length = searchPattern.Length -> ValueSome(PatternMatch(PatternMatchKind.Exact, false, false)) + | 0 -> ValueSome(PatternMatch(PatternMatchKind.Prefix, false, false)) + | _ -> ValueNone else // full name with dots allows for path matching, e.g. // "f.c.so.elseif" will match "Fantomas.Core.SyntaxOak.ElseIfNode" - patternMatcher.TryMatch $"{item.Container.FullName}.{name}" |> Option.ofNullable + patternMatcher.TryMatch $"{item.Container.FullName}.{name}" + |> ValueOption.ofNullable - let processDocument (tryMatch: NavigableItem -> PatternMatch option) (kinds: IImmutableSet) (document: Document) = + let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! sourceText = document.GetTextAsync ct - let processItem (item: NavigableItem) = - asyncMaybe { // TODO: make a flat cancellable task - - do! Option.guard (kinds.Contains(navigateToItemKindToRoslynKind item.Kind)) - - let! m = tryMatch item - - let! sourceSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) - let glyph = navigateToItemKindToGlyph item.Kind - let kind = navigateToItemKindToRoslynKind item.Kind - let additionalInfo = formatInfo item.Container document - - return - FSharpNavigateToSearchResult( - additionalInfo, - kind, - patternMatchKindToNavigateToMatchKind m.Kind, - item.Name, - FSharpNavigableItem(glyph, ImmutableArray.Create(TaggedText(TextTags.Text, item.Name)), document, sourceSpan) - ) - } - let! items = getNavigableItems document - let! processed = items |> Seq.map processItem |> Async.Parallel - return processed |> Array.choose id + + let processed = + [| + for item in items do + let contains = kinds.Contains(navigateToItemKindToRoslynKind item.Kind) + let patternMatch = tryMatch item + + match contains, patternMatch with + | true, ValueSome m -> + let sourceSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, item.Range) + + match sourceSpan with + | ValueNone -> () + | ValueSome sourceSpan -> + let glyph = navigateToItemKindToGlyph item.Kind + let kind = navigateToItemKindToRoslynKind item.Kind + let additionalInfo = formatInfo item.Container document + + yield + FSharpNavigateToSearchResult( + additionalInfo, + kind, + patternMatchKindToNavigateToMatchKind m.Kind, + item.Name, + FSharpNavigableItem( + glyph, + ImmutableArray.Create(TaggedText(TextTags.Text, item.Name)), + document, + sourceSpan + ) + ) + | _ -> () + |] + + return processed } interface IFSharpNavigateToSearchService with @@ -190,7 +202,10 @@ type internal FSharpNavigateToSearchService [] let tryMatch = createMatcherFor searchPattern let tasks = - Seq.map (fun doc -> processDocument tryMatch kinds doc) project.Documents + [| + for doc in project.Documents do + yield processDocument tryMatch kinds doc + |] let! results = CancellableTask.whenAll tasks @@ -205,16 +220,10 @@ type internal FSharpNavigateToSearchService [] } |> CancellableTask.start cancellationToken - member _.SearchDocumentAsync - ( - document: Document, - searchPattern, - kinds, - cancellationToken - ) : Task> = + member _.SearchDocumentAsync(document: Document, searchPattern, kinds, cancellationToken) = cancellableTask { let! result = processDocument (createMatcherFor searchPattern) kinds document - return result |> Array.toImmutableArray + return Array.toImmutableArray result } |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigationBarItemService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigationBarItemService.fs index 9173dd5f863..b31b42c9822 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigationBarItemService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigationBarItemService.fs @@ -9,6 +9,7 @@ open System.Threading.Tasks open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor open FSharp.Compiler.EditorServices +open CancellableTasks type internal NavigationBarSymbolItem(text, glyph, spans, childItems) = inherit FSharpNavigationBarItem(text, glyph, spans, childItems) @@ -20,10 +21,11 @@ type internal FSharpNavigationBarItemService [] () = interface IFSharpNavigationBarItemService with member _.GetItemsAsync(document, cancellationToken) : Task> = - asyncMaybe { - let! parseResults = - document.GetFSharpParseResultsAsync(nameof (FSharpNavigationBarItemService)) - |> liftAsync + cancellableTask { + + let! cancellationToken = CancellableTask.getCancellationToken () + + let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpNavigationBarItemService)) let navItems = Navigation.getNavigation parseResults.ParseTree let! sourceText = document.GetTextAsync(cancellationToken) @@ -31,27 +33,30 @@ type internal FSharpNavigationBarItemService [] () = let rangeToTextSpan range = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) - return - navItems.Declarations - |> Array.choose (fun topLevelDecl -> - rangeToTextSpan (topLevelDecl.Declaration.Range) - |> Option.map (fun topLevelTextSpan -> - let childItems = - topLevelDecl.Nested - |> Array.choose (fun decl -> - rangeToTextSpan (decl.Range) - |> Option.map (fun textSpan -> - NavigationBarSymbolItem(decl.LogicalName, decl.RoslynGlyph, [| textSpan |], null) - :> FSharpNavigationBarItem)) - - NavigationBarSymbolItem( - topLevelDecl.Declaration.LogicalName, - topLevelDecl.Declaration.RoslynGlyph, - [| topLevelTextSpan |], - childItems - ) - :> FSharpNavigationBarItem)) - :> IList<_> + if navItems.Declarations.Length = 0 then + return emptyResult + else + + return + navItems.Declarations + |> Array.chooseV (fun topLevelDecl -> + rangeToTextSpan (topLevelDecl.Declaration.Range) + |> ValueOption.map (fun topLevelTextSpan -> + let childItems = + topLevelDecl.Nested + |> Array.chooseV (fun decl -> + rangeToTextSpan (decl.Range) + |> ValueOption.map (fun textSpan -> + NavigationBarSymbolItem(decl.LogicalName, decl.RoslynGlyph, [| textSpan |], null) + :> FSharpNavigationBarItem)) + + NavigationBarSymbolItem( + topLevelDecl.Declaration.LogicalName, + topLevelDecl.Declaration.RoslynGlyph, + [| topLevelTextSpan |], + childItems + ) + :> FSharpNavigationBarItem)) + :> IList<_> } - |> Async.map (Option.defaultValue emptyResult) - |> RoslynHelpers.StartAsyncAsTask(cancellationToken) + |> CancellableTask.start cancellationToken diff --git a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs index 7fb51b9f83b..6e7df021a2c 100644 --- a/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs +++ b/vsintegration/src/FSharp.Editor/QuickInfo/QuickInfoProvider.fs @@ -64,8 +64,8 @@ type internal FSharpAsyncQuickInfoSource let textSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, lexerSymbol.Range) match textSpan with - | None -> return None - | Some textSpan -> + | ValueNone -> return None + | ValueSome textSpan -> let trackingSpan = textBuffer.CurrentSnapshot.CreateTrackingSpan(textSpan.Start, textSpan.Length, SpanTrackingMode.EdgeInclusive) diff --git a/vsintegration/src/FSharp.Editor/Refactor/AddExplicitTypeToParameter.fs b/vsintegration/src/FSharp.Editor/Refactor/AddExplicitTypeToParameter.fs index a018bb52558..bcea39dd8ec 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/AddExplicitTypeToParameter.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/AddExplicitTypeToParameter.fs @@ -6,7 +6,6 @@ open System open System.Composition open System.Threading -open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols open FSharp.Compiler.Text @@ -40,6 +39,8 @@ type internal FSharpAddExplicitTypeToParameterRefactoring [ CancellableTask.start ct + |> Async.AwaitTask let! parseFileResults, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(nameof (FSharpAddExplicitTypeToParameterRefactoring)) diff --git a/vsintegration/src/FSharp.Editor/Refactor/ChangeDerefToValueRefactoring.fs b/vsintegration/src/FSharp.Editor/Refactor/ChangeDerefToValueRefactoring.fs index 59e7288b623..07408c049b1 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/ChangeDerefToValueRefactoring.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/ChangeDerefToValueRefactoring.fs @@ -15,6 +15,7 @@ open FSharp.Compiler.Syntax open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeRefactorings open Microsoft.CodeAnalysis.CodeActions +open CancellableTasks [] type internal FSharpChangeDerefToValueRefactoring [] () = @@ -27,6 +28,8 @@ type internal FSharpChangeDerefToValueRefactoring [] () = let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpChangeDerefToValueRefactoring)) + |> CancellableTask.start context.CancellationToken + |> Async.AwaitTask |> liftAsync let selectionRange = diff --git a/vsintegration/src/FSharp.Editor/Refactor/ChangeTypeofWithNameToNameofExpression.fs b/vsintegration/src/FSharp.Editor/Refactor/ChangeTypeofWithNameToNameofExpression.fs index d1559708f23..6cc93b06d87 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/ChangeTypeofWithNameToNameofExpression.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/ChangeTypeofWithNameToNameofExpression.fs @@ -15,6 +15,7 @@ open FSharp.Compiler.Syntax open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeRefactorings open Microsoft.CodeAnalysis.CodeActions +open CancellableTasks [] type internal FSharpChangeTypeofWithNameToNameofExpressionRefactoring [] () = @@ -27,6 +28,8 @@ type internal FSharpChangeTypeofWithNameToNameofExpressionRefactoring [ CancellableTask.start context.CancellationToken + |> Async.AwaitTask |> liftAsync let selectionRange = diff --git a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs index ac35e69af22..f96ea848c1f 100644 --- a/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs +++ b/vsintegration/src/FSharp.Editor/Structure/BlockStructureService.fs @@ -115,12 +115,15 @@ module internal BlockStructure = | Scope.While | Scope.For -> false + [] + let ellipsis = "..." + let createBlockSpans isBlockStructureEnabled (sourceText: SourceText) (parsedInput: ParsedInput) = let linetext = sourceText.Lines |> Seq.map (fun x -> x.ToString()) |> Seq.toArray Structure.getOutliningRanges linetext parsedInput |> Seq.distinctBy (fun x -> x.Range.StartLine) - |> Seq.choose (fun scopeRange -> + |> Seq.chooseV (fun scopeRange -> // the range of text to collapse let textSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, scopeRange.CollapseRange) @@ -128,13 +131,13 @@ module internal BlockStructure = let hintSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, scopeRange.Range) match textSpan, hintSpan with - | Some textSpan, Some hintSpan -> + | ValueSome textSpan, ValueSome hintSpan -> let line = sourceText.Lines.GetLineFromPosition textSpan.Start let bannerText = - match Option.ofNullable (line.Span.Intersection textSpan) with - | Some span -> sourceText.GetSubText(span).ToString() + "..." - | None -> "..." + match ValueOption.ofNullable (line.Span.Intersection textSpan) with + | ValueSome span -> sourceText.GetSubText(span).ToString() + ellipsis + | ValueNone -> ellipsis let blockType = if isBlockStructureEnabled then @@ -142,8 +145,10 @@ module internal BlockStructure = else FSharpBlockTypes.Nonstructural - Some(FSharpBlockSpan(blockType, true, textSpan, hintSpan, bannerText, autoCollapse = isAutoCollapsible scopeRange.Scope)) - | _, _ -> None) + ValueSome( + FSharpBlockSpan(blockType, true, textSpan, hintSpan, bannerText, autoCollapse = isAutoCollapsible scopeRange.Scope) + ) + | _, _ -> ValueNone) open BlockStructure open CancellableTasks diff --git a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs index fae5dde7797..2a520d72066 100644 --- a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs +++ b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs @@ -48,6 +48,9 @@ module TelemetryEvents = [] let GoToDefinitionGetSymbol = "gotodefinition/getsymbol" + [] + let AnalysisSaveFileHandler = "analysis/savefilehandler" + // TODO: needs to be something more sophisticated in future [] type TelemetryThrottlingStrategy = @@ -108,7 +111,7 @@ type TelemetryReporter private (name: string, props: (string * obj) array, stopw static member ReportFault(name, ?severity: FaultSeverity, ?e: exn) = if TelemetryReporter.SendAdditionalTelemetry.Value then - let faultName = String.Concat(name, "/fault") + let faultName = String.Concat(TelemetryReporter.eventPrefix, name, "/fault") match severity, e with | Some s, Some e -> TelemetryService.DefaultSession.PostFault(faultName, name, s, e) @@ -120,7 +123,7 @@ type TelemetryReporter private (name: string, props: (string * obj) array, stopw static member ReportCustomFailure(name, ?props) = if TelemetryReporter.SendAdditionalTelemetry.Value then let props = defaultArg props [||] - let name = String.Concat(name, "/failure") + let name = String.Concat(TelemetryReporter.eventPrefix, name, "/failure") let event = TelemetryReporter.createEvent name props TelemetryService.DefaultSession.PostEvent event diff --git a/vsintegration/tests/FSharp.Editor.Tests/BreakpointResolutionServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/BreakpointResolutionServiceTests.fs index 08a73dc2816..cd01e6434d7 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/BreakpointResolutionServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/BreakpointResolutionServiceTests.fs @@ -66,8 +66,8 @@ let main argv = task.Result match actualResolutionOption with - | None -> Assert.True(expectedResolution.IsNone, "BreakpointResolutionService failed to resolve breakpoint position") - | Some (actualResolutionRange) -> + | ValueNone -> Assert.True(expectedResolution.IsNone, "BreakpointResolutionService failed to resolve breakpoint position") + | ValueSome (actualResolutionRange) -> let actualResolution = sourceText .GetSubText(RoslynHelpers.FSharpRangeToTextSpan(sourceText, actualResolutionRange)) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs index 71fb3b0c963..0041a7dd4bb 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CompletionProviderTests.fs @@ -34,7 +34,7 @@ module CompletionProviderTests = let results = let task = - FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [])) + FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [||])) |> CancellableTask.start CancellationToken.None task.Result |> Seq.map (fun result -> result.DisplayText) @@ -82,7 +82,7 @@ module CompletionProviderTests = let actual = let task = - FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [])) + FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [||])) |> CancellableTask.start CancellationToken.None task.Result diff --git a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs index 7f3d4d0e804..4a10dacc6c6 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FindReferencesTests.fs @@ -71,7 +71,7 @@ module FindReferences = let document = solution.TryGetDocumentFromPath documentPath - |> Option.defaultWith (fun _ -> failwith "Document not found") + |> ValueOption.defaultWith (fun _ -> failwith "Document not found") findUsagesService .FindReferencesAsync(document, getPositionOf "funcParam" documentPath, context) @@ -94,7 +94,7 @@ module FindReferences = let document = solution.TryGetDocumentFromPath documentPath - |> Option.defaultWith (fun _ -> failwith "Document not found") + |> ValueOption.defaultWith (fun _ -> failwith "Document not found") findUsagesService .FindReferencesAsync(document, getPositionOf "funcParam" documentPath, context) @@ -116,7 +116,7 @@ module FindReferences = let document = solution.TryGetDocumentFromPath documentPath - |> Option.defaultWith (fun _ -> failwith "Document not found") + |> ValueOption.defaultWith (fun _ -> failwith "Document not found") findUsagesService .FindReferencesAsync(document, getPositionOf "sharedFunc" documentPath, context) @@ -157,7 +157,7 @@ module FindReferences = let document = solution2.TryGetDocumentFromPath documentPath - |> Option.defaultWith (fun _ -> failwith "Document not found") + |> ValueOption.defaultWith (fun _ -> failwith "Document not found") findUsagesService .FindReferencesAsync(document, getPositionOf operator documentPath, context) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FsxCompletionProviderTests.fs b/vsintegration/tests/FSharp.Editor.Tests/FsxCompletionProviderTests.fs index 49955dedbb4..9d9432aaf13 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FsxCompletionProviderTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/FsxCompletionProviderTests.fs @@ -32,7 +32,7 @@ type Worker() = let actual = let x = - FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [])) + FSharpCompletionProvider.ProvideCompletionsAsyncAux(document, caretPosition, (fun _ -> [||])) |> CancellableTask.start CancellationToken.None x.Result