From 3ef425bcfdac8e8286272b53b72abbf722a7cd8c Mon Sep 17 00:00:00 2001 From: realvictorprm Date: Sun, 30 Apr 2017 22:49:38 +0200 Subject: [PATCH 1/6] Improved scrolling performance totally. Editing performance improved sligthly. Smooth scrolling is now possible without any problems! Enjoy it! --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 347 +++++------------- 1 file changed, 102 insertions(+), 245 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index afe4d105913..b2d565d16b3 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -27,192 +27,40 @@ open Internal.Utilities.StructuredFormat open Microsoft.VisualStudio.Text.Tagging open System.Collections.Concurrent open System.Collections - - -//type internal CodeLensAdornment -// ( -// workspace: Workspace, -// documentId: Lazy, -// view: IWpfTextView, -// checker: FSharpChecker, -// projectInfoManager: ProjectInfoManager, -// typeMap: Lazy, -// gotoDefinitionService: FSharpGoToDefinitionService -// ) as self = - -// let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" -// let codeLensLines = ConcurrentDictionary() - -// do assert (documentId <> null) - -// let mutable cancellationTokenSource = new CancellationTokenSource() -// let mutable cancellationToken = cancellationTokenSource.Token - -// /// Get the interline layer. CodeLens belong there. -// let interlineLayer = view.GetAdornmentLayer(PredefinedAdornmentLayers.InterLine) -// do view.LayoutChanged.AddHandler (fun _ e -> self.OnLayoutChanged e) - -// let layoutTagToFormatting (layoutTag: LayoutTag) = -// layoutTag -// |> RoslynHelpers.roslynTag -// |> ClassificationTags.GetClassificationTypeName -// |> typeMap.Value.GetClassificationType -// |> formatMap.Value.GetTextProperties - -// let executeCodeLenseAsync () = -// let uiContext = SynchronizationContext.Current -// asyncMaybe { -// try -// let! document = workspace.CurrentSolution.GetDocument(documentId.Value) |> Option.ofObj -// let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) -// let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) -// let! symbolUses = checkFileResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync -// do! Async.SwitchToContext uiContext |> liftAsync - -// let applyCodeLens bufferPosition (taggedText: seq) m = -// let line = view.TextViewLines.GetTextViewLineContainingBufferPosition(bufferPosition) - -// let offset = -// [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) -// |> Option.defaultValue 0 - -// let realStart = line.Start.Add(offset) -// let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) -// let geometry = view.TextViewLines.GetMarkerGeometry(span) -// let textBox = TextBlock(Width = 500., Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) -// DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) -// let navigation = QuickInfoNavigation(gotoDefinitionService, document, m) -// for text in taggedText do -// let run = Documents.Run text.Text -// DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) -// let inl = -// match text with -// | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> -// let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) -// h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) -// h :> Documents.Inline -// | _ -> run :> _ -// textBox.Inlines.Add inl - -// Canvas.SetLeft(textBox, geometry.Bounds.Left) -// Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) -// let tag = IntraTextAdornmentTag(textBox, (fun _ _ -> ())) -// let adornment = SpaceNegotiatingAdornmentTag(0., 0., 0., 0., 0., PositionAffinity.Predecessor, tag, tag) -// interlineLayer.AddAdornment(AdornmentPositioningBehavior.TextRelative, Nullable span, adornment, textBox, null) |> ignore -// if line.VisibilityState = VisibilityState.Unattached then view.DisplayTextLineContainingBufferPosition(line.Start, 0., ViewRelativePosition.Top) - -// let useResults (displayContext: FSharpDisplayContext, func: FSharpMemberOrFunctionOrValue) = -// async { -// let lineNumber = Line.toZ func.DeclarationLocation.StartLine - -// if (lineNumber >= 0 || lineNumber < view.TextSnapshot.LineCount) && -// not func.IsPropertyGetterMethod && -// not func.IsPropertySetterMethod then - -// match func.FullTypeSafe with -// | Some ty -> -// let bufferPosition = view.TextSnapshot.GetLineFromLineNumber(lineNumber).Start -// if not (codeLensLines.ContainsKey lineNumber) then -// let! displayEnv = checkFileResults.GetDisplayEnvForPos(func.DeclarationLocation.Start) - -// let displayContext = -// match displayEnv with -// | Some denv -> FSharpDisplayContext(fun _ -> denv) -// | None -> displayContext - -// let typeLayout = ty.FormatLayout(displayContext) -// let taggedText = ResizeArray() -// Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore -// codeLensLines.[lineNumber] <- taggedText -// applyCodeLens bufferPosition taggedText func.DeclarationLocation -// | None -> () -// } - -// //let forceReformat () = -// // view.VisualSnapshot.Lines -// // |> Seq.iter(fun line -> view.DisplayTextLineContainingBufferPosition(line.Start, 25., ViewRelativePosition.Top)) - -// for symbolUse in symbolUses do -// if symbolUse.IsFromDefinition then -// match symbolUse.Symbol with -// | :? FSharpEntity as entity -> -// for func in entity.MembersFunctionsAndValues do -// do! useResults (symbolUse.DisplayContext, func) |> liftAsync -// | _ -> () -// with -// | _ -> () // TODO: Should report error -// } - -// /// Handles required transformation depending on whether CodeLens are required or not required -// interface ILineTransformSource with -// override __.GetLineTransform(line, _, _) = -// let applyCodeLens = codeLensLines.ContainsKey(view.TextSnapshot.GetLineNumberFromPosition(line.Start.Position)) -// if applyCodeLens then -// // Give us space for CodeLens -// LineTransform(15., 1., 1.) -// else -// // Restore old transformation -// line.DefaultLineTransform - -// member __.OnLayoutChanged (e:TextViewLayoutChangedEventArgs) = -// // Non expensive computations which have to be done immediate -// for line in e.NewOrReformattedLines do -// let lineNumber = view.TextSnapshot.GetLineNumberFromPosition(line.Start.Position) -// codeLensLines.TryRemove(lineNumber) |> ignore //All changed lines are supposed to be now No-CodeLens-Lines (Reset) -// if line.VisibilityState = VisibilityState.Unattached then -// view.DisplayTextLineContainingBufferPosition(line.Start, 0., ViewRelativePosition.Top) //Force refresh (works partly...) - -// //for line in view.TextViewLines.WpfTextViewLines do -// // if line.VisibilityState = VisibilityState.Unattached then -// // view.DisplayTextLineContainingBufferPosition(line.Start, 0., ViewRelativePosition.Top) //Force refresh (works partly...) - -// cancellationTokenSource.Cancel() // Stop all ongoing async workflow. -// cancellationTokenSource.Dispose() -// cancellationTokenSource <- new CancellationTokenSource() -// cancellationToken <- cancellationTokenSource.Token -// executeCodeLenseAsync() |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationToken - - +open System.Windows.Media.Animation -type CodeLensTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, identityTag:obj, providerTag:obj, text) = - inherit SpaceNegotiatingAdornmentTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, identityTag, providerTag) - - new (width, topSpace, baseline, textHeight, bottomSpace, affinity, identityTag:obj, providerTag:obj) = - new CodeLensTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, identityTag, providerTag, "Lalala") - - member val Text = text with get, set - +type CodeLensTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag:obj, providerTag:obj) = + inherit SpaceNegotiatingAdornmentTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag, providerTag) -type internal CodeLensTagger +type internal CodeLensTagger ( - workspace: Workspace, + workspace: Workspace, documentId: Lazy, buffer: ITextBuffer, checker: FSharpChecker, projectInfoManager: ProjectInfoManager, - __: Lazy, - ___: FSharpGoToDefinitionService + typeMap: Lazy, + gotoDefinitionService: FSharpGoToDefinitionService ) as self = let tagsChanged = new Event,SnapshotSpanEventArgs>() - //let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" - let codeLensLines = ConcurrentDictionary() + let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" + let mutable codeLensLines = ConcurrentDictionary() do assert (documentId <> null) let mutable cancellationTokenSourceBufferChanged = new CancellationTokenSource() let mutable cancellationTokenBufferChanged = cancellationTokenSourceBufferChanged.Token - let mutable cancellationTokenSourceLayoutChanged = new CancellationTokenSource() - //let layoutTagToFormatting (layoutTag: LayoutTag) = - // layoutTag - // |> RoslynHelpers.roslynTag - // |> ClassificationTags.GetClassificationTypeName - // |> typeMap.Value.GetClassificationType - // |> formatMap.Value.GetTextProperties - - let executeCodeLenseAsync () = - let uiContext = SynchronizationContext.Current + let layoutTagToFormatting (layoutTag: LayoutTag) = + layoutTag + |> RoslynHelpers.roslynTag + |> ClassificationTags.GetClassificationTypeName + |> typeMap.Value.GetClassificationType + |> formatMap.Value.GetTextProperties + + let executeCodeLenseAsync (__:TextContentChangedEventArgs) = + //let uiContext = SynchronizationContext.Current asyncMaybe { try Logging.Logging.logInfof "Rechecking code due to buffer edit!" @@ -221,11 +69,11 @@ type internal CodeLensTagger let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) let! symbolUses = checkFileResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync - let view = self.WpfTextView.Value; let textSnapshot = view.TextSnapshot.TextBuffer.CurrentSnapshot Logging.Logging.logInfof "Updating code lens due to buffer edit!" - let results = Concurrent.ConcurrentDictionary<_,_>() + codeLensLines.Clear(); + //let results = Concurrent.ConcurrentDictionary<_,_>() let useResults (displayContext: FSharpDisplayContext, func: FSharpMemberOrFunctionOrValue) = async { let lineNumber = Line.toZ func.DeclarationLocation.StartLine @@ -246,8 +94,11 @@ type internal CodeLensTagger let typeLayout = ty.FormatLayout(displayContext) let taggedText = ResizeArray() Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore - - results.[lineNumber] <- [ for t in taggedText do yield t.Text ]|> String.concat "" + + let navigation = QuickInfoNavigation(gotoDefinitionService, document, func.DeclarationLocation) + codeLensLines.[int lineNumber] <- (taggedText, navigation) + let line = textSnapshot.GetLineFromLineNumber(lineNumber) + tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) | None -> () } @@ -256,82 +107,94 @@ type internal CodeLensTagger match symbolUse.Symbol with | :? FSharpEntity as entity -> for func in entity.MembersFunctionsAndValues do - do! useResults (symbolUse.DisplayContext, func) |> liftAsync + do useResults (symbolUse.DisplayContext, func) |> Async.Start | _ -> () - Logging.Logging.logInfof "Extraction complete, creating code lens data on UI thread!" - codeLensLines.Clear() - let tags:Concurrent.ConcurrentDictionary<_,_> = self.Tags - let adornments:ConcurrentDictionary<_,_> = self.Adornments - adornments.Clear() - tags.Clear() - do! Async.SwitchToContext uiContext |> liftAsync - for r in results do - let label = new Label() - label.Width <- 500. - label.Height <- 50. - label.Content <- r.Value - codeLensLines.[r.Key] <- label - Logging.Logging.logInfof "Finished updating code lens." - view.DisplayTextLineContainingBufferPosition(view.TextViewLines.FirstVisibleLine.Start, 0., ViewRelativePosition.Top) + Logging.Logging.logInfof "Extraction complete, replacing old code lens data!" + //do! Async.SwitchToContext uiContext |> liftAsync + //let handler = tagsChanged; + //handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) + Logging.Logging.logInfof "Finished updating code lens." |> ignore with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () + | ex -> Logging.Logging.logErrorf "Error occured: %A" ex + } - - - - do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) - + + let createCodeLensUIElementByLine (line:ITextViewLine) = + let view = self.WpfTextView.Value + let offset = + [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) + |> Option.defaultValue 0 + let realStart = line.Start.Add(offset) + let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) + let geometry = view.TextViewLines.GetMarkerGeometry(span) + let taggedText, navigation = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] + let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) + DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) + for text in taggedText do + let run = Documents.Run text.Text + DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) + let inl = + match text with + | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> + let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + textBox.Inlines.Add inl + Canvas.SetLeft(textBox, geometry.Bounds.Left) + Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) + textBox - member __.BufferChanged _ = + do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) + member __.BufferChanged e = cancellationTokenSourceBufferChanged.Cancel() // Stop all ongoing async workflow. cancellationTokenSourceBufferChanged.Dispose() cancellationTokenSourceBufferChanged <- new CancellationTokenSource() cancellationTokenBufferChanged <- cancellationTokenSourceBufferChanged.Token - executeCodeLenseAsync() |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenBufferChanged + executeCodeLenseAsync e |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenBufferChanged member val WpfTextView : Lazy = Lazy() with get,set member val CodeLensLayer = Lazy() with get, set + + member t.T = "" - member __.Adornments = ConcurrentDictionary<_,_>() - - member __.LayoutChanged (e:TextViewLayoutChangedEventArgs) = + member __.LayoutChanged (e:TextViewLayoutChangedEventArgs) = let uiContext = SynchronizationContext.Current - cancellationTokenSourceLayoutChanged.Cancel() - cancellationTokenSourceLayoutChanged.Dispose() - cancellationTokenSourceLayoutChanged <- new CancellationTokenSource() + + //cancellationTokenSourceLayoutChanged.Cancel() + //cancellationTokenSourceLayoutChanged.Dispose() + //cancellationTokenSourceLayoutChanged <- new CancellationTokenSource() async{ - do Async.Sleep(50) |> ignore - do! Async.SwitchToContext uiContext - for line in e.NewOrReformattedLines do - if not(self.Adornments.ContainsKey(buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position))) then - let tags = line.GetAdornmentTags self - let tag = tags |> Seq.tryHead - match tag with - | None -> () - | Some t -> - try - if not self.CodeLensLayer.IsValueCreated then - self.CodeLensLayer.Force() |> ignore - let layer = self.CodeLensLayer.Value - let offset = - [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) - |> Option.defaultValue 0 - let view = self.WpfTextView.Value - let realStart = line.Start.Add(offset) - let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) - let geometry = view.TextViewLines.GetMarkerGeometry(span) - let ui = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] - Canvas.SetLeft(ui, geometry.Bounds.Left) - Canvas.SetTop(ui, geometry.Bounds.Top - 15.) - layer.AddAdornment(line.Extent, t, ui) |> ignore - self.Adornments.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] <- ui - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () - } |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenSourceLayoutChanged.Token + + try + do! Async.Sleep(1000) //Wait before we add it and check whether the lines are still valid (so visible) if not, nothing will happen + do! Async.SwitchToContext uiContext + let view = self.WpfTextView.Value + let visibleSnapshot = SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End) + for line in e.NewOrReformattedLines do + if line.IsValid && visibleSnapshot.Contains(line.Extent) then + let tags = line.GetAdornmentTags self + let tagOption = tags |> Seq.tryHead + match tagOption with + | None -> () + | Some tag -> + try + if not self.CodeLensLayer.IsValueCreated then + self.CodeLensLayer.Force() |> ignore + let layer = self.CodeLensLayer.Value + let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) + let textBox = createCodeLensUIElementByLine line + layer.AddAdornment(line.Extent, tag, textBox) |> ignore + do textBox.BeginAnimation(UIElement.OpacityProperty, da) + with + | e -> Logging.Logging.logErrorf "Error occured: %A" e + () + with + | e -> Logging.Logging.logErrorf "Error occured: %A" e + () + } |> Async.Start () member __.Tags = new Concurrent.ConcurrentDictionary<_, _>() @@ -346,18 +209,12 @@ type internal CodeLensTagger new NormalizedSnapshotSpanCollection( spans |> Seq.map (fun span -> span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive))) - //Logging.Logging.logMsgf "Iterating over %A spans" spans.Count for tagSpan in translatedSpans do - if self.WpfTextView.IsValueCreated then - let lineNumber = buffer.CurrentSnapshot.GetLineNumberFromPosition(tagSpan.Start.Position) - let codeLens = codeLensLines - if codeLens.ContainsKey(lineNumber) then - if self.Tags.ContainsKey(lineNumber) then - yield self.Tags.[lineNumber] - else - let res = TagSpan(tagSpan, CodeLensTag(0., 15., 0., 0., 0., PositionAffinity.Predecessor, self, self)):> ITagSpan - self.Tags.[lineNumber] <- res - yield res + let lineNumber = buffer.CurrentSnapshot.GetLineNumberFromPosition(tagSpan.Start.Position) + let codeLens = codeLensLines + if codeLens.ContainsKey(lineNumber) then + let res = TagSpan(tagSpan, CodeLensTag(0., 15., 0., 0., 0., PositionAffinity.Predecessor, self, self)):> ITagSpan + yield res } From 3642b3e3f727958fb136c5a0d3a79312ff6cca0f Mon Sep 17 00:00:00 2001 From: realvictorprm Date: Sun, 30 Apr 2017 23:49:32 +0200 Subject: [PATCH 2/6] Further big performance improvements. --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 89 ++++++++++++------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index b2d565d16b3..8300d108f4e 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -32,7 +32,7 @@ open System.Windows.Media.Animation type CodeLensTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag:obj, providerTag:obj) = inherit SpaceNegotiatingAdornmentTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag, providerTag) -type internal CodeLensTagger +type internal CodeLensTagger ( workspace: Workspace, documentId: Lazy, @@ -60,13 +60,15 @@ type internal CodeLensTagger |> formatMap.Value.GetTextProperties let executeCodeLenseAsync (__:TextContentChangedEventArgs) = - //let uiContext = SynchronizationContext.Current + let uiContext = SynchronizationContext.Current asyncMaybe { try + Async.Sleep(1000) |> Async.RunSynchronously Logging.Logging.logInfof "Rechecking code due to buffer edit!" let! document = workspace.CurrentSolution.GetDocument(documentId.Value) |> Option.ofObj let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) + Logging.Logging.logInfof "Getting uses of all symbols!" let! symbolUses = checkFileResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync let view = self.WpfTextView.Value; @@ -96,10 +98,9 @@ type internal CodeLensTagger Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore let navigation = QuickInfoNavigation(gotoDefinitionService, document, func.DeclarationLocation) - codeLensLines.[int lineNumber] <- (taggedText, navigation) - let line = textSnapshot.GetLineFromLineNumber(lineNumber) - tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) - | None -> () + return Some taggedText, Some navigation + | None -> return None, None + else return None, None } for symbolUse in symbolUses do @@ -107,12 +108,15 @@ type internal CodeLensTagger match symbolUse.Symbol with | :? FSharpEntity as entity -> for func in entity.MembersFunctionsAndValues do - do useResults (symbolUse.DisplayContext, func) |> Async.Start + let lineNumber = Line.toZ func.DeclarationLocation.StartLine + codeLensLines.[int lineNumber] <- Async.cache (useResults (symbolUse.DisplayContext, func)) + //let line = textSnapshot.GetLineFromLineNumber(lineNumber) + //tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) | _ -> () Logging.Logging.logInfof "Extraction complete, replacing old code lens data!" - //do! Async.SwitchToContext uiContext |> liftAsync - //let handler = tagsChanged; - //handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) + do! Async.SwitchToContext uiContext |> liftAsync + let handler = tagsChanged; + handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) Logging.Logging.logInfof "Finished updating code lens." |> ignore with | ex -> Logging.Logging.logErrorf "Error occured: %A" ex @@ -127,23 +131,26 @@ type internal CodeLensTagger let realStart = line.Start.Add(offset) let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) let geometry = view.TextViewLines.GetMarkerGeometry(span) - let taggedText, navigation = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] - let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) - DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) - for text in taggedText do - let run = Documents.Run text.Text - DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) - let inl = - match text with - | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> - let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) - h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) - h :> Documents.Inline - | _ -> run :> _ - textBox.Inlines.Add inl - Canvas.SetLeft(textBox, geometry.Bounds.Left) - Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) - textBox + let (taggedTextOption, navigationOption) = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] |> Async.RunSynchronously + match taggedTextOption, navigationOption with + | Some taggedText, Some navigation -> + let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) + DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) + for text in taggedText do + let run = Documents.Run text.Text + DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) + let inl = + match text with + | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> + let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + textBox.Inlines.Add inl + Canvas.SetLeft(textBox, geometry.Bounds.Left) + Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) + Some textBox + | _, _ -> None do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) @@ -171,10 +178,8 @@ type internal CodeLensTagger try do! Async.Sleep(1000) //Wait before we add it and check whether the lines are still valid (so visible) if not, nothing will happen do! Async.SwitchToContext uiContext - let view = self.WpfTextView.Value - let visibleSnapshot = SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End) for line in e.NewOrReformattedLines do - if line.IsValid && visibleSnapshot.Contains(line.Extent) then + if line.IsValid then let tags = line.GetAdornmentTags self let tagOption = tags |> Seq.tryHead match tagOption with @@ -185,9 +190,12 @@ type internal CodeLensTagger self.CodeLensLayer.Force() |> ignore let layer = self.CodeLensLayer.Value let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) - let textBox = createCodeLensUIElementByLine line - layer.AddAdornment(line.Extent, tag, textBox) |> ignore - do textBox.BeginAnimation(UIElement.OpacityProperty, da) + let res = createCodeLensUIElementByLine line + match res with + | Some textBox -> + layer.AddAdornment(line.Extent, tag, textBox) |> ignore + do textBox.BeginAnimation(UIElement.OpacityProperty, da) + | None -> () with | e -> Logging.Logging.logErrorf "Error occured: %A" e () @@ -275,4 +283,17 @@ type internal CodeLensProvider interface ITaggerProvider with override __.CreateTagger(buffer) = let tagger = getSuitableAdornmentProvider buffer - box (tagger) :?> _ \ No newline at end of file + box (tagger) :?> _ + + + +//module Test = +// let t = "" + + +// type Cherry (s, a, b) = +// let s = s +// let b = b +// let a = a + +// let s = "" \ No newline at end of file From dec7a1a17fb43b3e6c6290b3a0ad9a758a02f4db Mon Sep 17 00:00:00 2001 From: realvictorprm Date: Mon, 1 May 2017 00:27:04 +0200 Subject: [PATCH 3/6] Code Lens are now shown as soon as possible. --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index 8300d108f4e..140cc16a420 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -46,6 +46,7 @@ type internal CodeLensTagger let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" let mutable codeLensLines = ConcurrentDictionary() + let mutable firstTimeChecked = false do assert (documentId <> null) @@ -59,7 +60,7 @@ type internal CodeLensTagger |> typeMap.Value.GetClassificationType |> formatMap.Value.GetTextProperties - let executeCodeLenseAsync (__:TextContentChangedEventArgs) = + let executeCodeLenseAsync () = let uiContext = SynchronizationContext.Current asyncMaybe { try @@ -118,11 +119,19 @@ type internal CodeLensTagger let handler = tagsChanged; handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) Logging.Logging.logInfof "Finished updating code lens." |> ignore + if not firstTimeChecked then + firstTimeChecked <- true with | ex -> Logging.Logging.logErrorf "Error occured: %A" ex } - + + do asyncMaybe{ + while not firstTimeChecked do + do! executeCodeLenseAsync() + Async.Sleep(1000) |> Async.RunSynchronously + } |> Async.Ignore |> Async.Start + let createCodeLensUIElementByLine (line:ITextViewLine) = let view = self.WpfTextView.Value let offset = @@ -154,12 +163,12 @@ type internal CodeLensTagger do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) - member __.BufferChanged e = + member __.BufferChanged ___ = cancellationTokenSourceBufferChanged.Cancel() // Stop all ongoing async workflow. cancellationTokenSourceBufferChanged.Dispose() cancellationTokenSourceBufferChanged <- new CancellationTokenSource() cancellationTokenBufferChanged <- cancellationTokenSourceBufferChanged.Token - executeCodeLenseAsync e |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenBufferChanged + executeCodeLenseAsync () |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenBufferChanged member val WpfTextView : Lazy = Lazy() with get,set @@ -280,12 +289,13 @@ type internal CodeLensProvider view.DisplayTextLineContainingBufferPosition(view.TextViewLines.FirstVisibleLine.Start, 0., ViewRelativePosition.Top) () - interface ITaggerProvider with + interface ITaggerProvider with override __.CreateTagger(buffer) = let tagger = getSuitableAdornmentProvider buffer box (tagger) :?> _ - +module Test = + let a = "a" //module Test = // let t = "" From 8758bdf0d508aaffc333c6d6d0b6a8d1692257f3 Mon Sep 17 00:00:00 2001 From: realvictorprm Date: Mon, 1 May 2017 12:41:14 +0200 Subject: [PATCH 4/6] Fixes, adjustments! 1. Code Lens should now be drawn correctly if they already have been visible. 2. Code Lens appear faster, wait time is reduced. --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 173 ++++++++++++------ 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index 140cc16a420..6cf4acfef64 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -52,6 +52,8 @@ type internal CodeLensTagger let mutable cancellationTokenSourceBufferChanged = new CancellationTokenSource() let mutable cancellationTokenBufferChanged = cancellationTokenSourceBufferChanged.Token + + let mutable cancellationTokenSourceLayoutChanged = new CancellationTokenSource() let layoutTagToFormatting (layoutTag: LayoutTag) = layoutTag @@ -140,27 +142,32 @@ type internal CodeLensTagger let realStart = line.Start.Add(offset) let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) let geometry = view.TextViewLines.GetMarkerGeometry(span) - let (taggedTextOption, navigationOption) = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] |> Async.RunSynchronously - match taggedTextOption, navigationOption with - | Some taggedText, Some navigation -> - let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) - DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) - for text in taggedText do - let run = Documents.Run text.Text - DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) - let inl = - match text with - | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> - let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) - h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) - h :> Documents.Inline - | _ -> run :> _ - textBox.Inlines.Add inl - Canvas.SetLeft(textBox, geometry.Bounds.Left) - Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) - Some textBox - | _, _ -> None - + if codeLensLines.ContainsKey(buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)) then + let (taggedTextOption, navigationOption) = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] |> Async.RunSynchronously + match taggedTextOption, navigationOption with + | Some taggedText, Some navigation -> + let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) + DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) + for text in taggedText do + let run = Documents.Run text.Text + DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) + let inl = + match text with + | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> + let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + textBox.Inlines.Add inl + Canvas.SetLeft(textBox, geometry.Bounds.Left) + Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) + textBox.Opacity <- 0.5 + Some textBox + | _, _ -> None + else + None + + let mutable visibleLines = list.Empty do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) member __.BufferChanged ___ = @@ -179,39 +186,99 @@ type internal CodeLensTagger member __.LayoutChanged (e:TextViewLayoutChangedEventArgs) = let uiContext = SynchronizationContext.Current - //cancellationTokenSourceLayoutChanged.Cancel() - //cancellationTokenSourceLayoutChanged.Dispose() - //cancellationTokenSourceLayoutChanged <- new CancellationTokenSource() - async{ - - try - do! Async.Sleep(1000) //Wait before we add it and check whether the lines are still valid (so visible) if not, nothing will happen - do! Async.SwitchToContext uiContext - for line in e.NewOrReformattedLines do - if line.IsValid then - let tags = line.GetAdornmentTags self - let tagOption = tags |> Seq.tryHead - match tagOption with - | None -> () - | Some tag -> - try - if not self.CodeLensLayer.IsValueCreated then - self.CodeLensLayer.Force() |> ignore - let layer = self.CodeLensLayer.Value - let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) - let res = createCodeLensUIElementByLine line - match res with - | Some textBox -> - layer.AddAdornment(line.Extent, tag, textBox) |> ignore - do textBox.BeginAnimation(UIElement.OpacityProperty, da) + cancellationTokenSourceLayoutChanged.Cancel() + cancellationTokenSourceLayoutChanged.Dispose() + cancellationTokenSourceLayoutChanged <- new CancellationTokenSource() + let asyncStuff = + async{ + if firstTimeChecked then + try + let s = async { + try + do! Async.SwitchToContext uiContext + let view = self.WpfTextView.Value + let buffer = view.TextBuffer.CurrentSnapshot + for line in e.NewOrReformattedLines do + if List.contains (buffer.GetLineNumberFromPosition(line.Start.Position)) visibleLines then + if not self.CodeLensLayer.IsValueCreated then + self.CodeLensLayer.Force() |> ignore + let layer = self.CodeLensLayer.Value + //let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) + let res = createCodeLensUIElementByLine line + match res with + | Some textBox -> + layer.AddAdornment(line.Extent, self, textBox) |> ignore + | None -> () + with + | e -> Logging.Logging.logErrorf "Error occured: %A" e + } + Async.Start (s, cancellationTokenSourceLayoutChanged.Token) + do! Async.Sleep(500) //Wait before we add it and check whether the lines are still valid (so visible) if not, nothing will happen + do! Async.SwitchToContext uiContext + let view = self.WpfTextView.Value + let firstLine, lastLine = + let firstLine, lastLine= (view.TextViewLines.FirstVisibleLine, view.TextViewLines.LastVisibleLine) + let buffer = view.TextBuffer.CurrentSnapshot + buffer.GetLineNumberFromPosition(firstLine.Start.Position), + buffer.GetLineNumberFromPosition(lastLine.Start.Position) + let buffer = view.TextBuffer.CurrentSnapshot + let realNewLines = e.NewOrReformattedLines |> Seq.filter (fun l -> not(List.contains (buffer.GetLineNumberFromPosition(l.Start.Position)) visibleLines)) + for line in realNewLines do + if line.IsValid then + Logging.Logging.logInfof "Redrawing line with number: %A" (view.TextBuffer.CurrentSnapshot.GetLineNumberFromPosition line.Start.Position) |> ignore + let tags = line.GetAdornmentTags self + let tagOption = tags |> Seq.tryHead + match tagOption with | None -> () - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () - } |> Async.Start + | Some __ -> + try + if not self.CodeLensLayer.IsValueCreated then + self.CodeLensLayer.Force() |> ignore + let layer = self.CodeLensLayer.Value + let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(0.8))) + let res = createCodeLensUIElementByLine line + match res with + | Some textBox -> + layer.AddAdornment(line.Extent, self, textBox) |> ignore + textBox.BeginAnimation(UIElement.OpacityProperty, da) + | None -> () + with + | e -> Logging.Logging.logErrorf "Error occured: %A" e + () + visibleLines <- [ firstLine .. lastLine] + //let visibleLineNumbers = [ firstLine .. lastLine] |> List.filter (fun l -> not (visibleLines |> List.contains l)) + //Logging.Logging.logInfof "Content of visibleLines: %A" visibleLines + //let lines = visibleLineNumbers + // |> List.map view.TextBuffer.CurrentSnapshot.GetLineFromLineNumber + // |> List.map (fun l -> view.GetTextViewLineContainingBufferPosition l.Start) + //for line in lines do + // if line.IsValid then + // Logging.Logging.logInfof "Redrawing line with number: %A" (view.TextBuffer.CurrentSnapshot.GetLineNumberFromPosition line.Start.Position) |> ignore + // let tags = line.GetAdornmentTags self + // let tagOption = tags |> Seq.tryHead + // match tagOption with + // | None -> () + // | Some __ -> + // try + // if not self.CodeLensLayer.IsValueCreated then + // self.CodeLensLayer.Force() |> ignore + // let layer = self.CodeLensLayer.Value + // let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) + // let res = createCodeLensUIElementByLine line + // match res with + // | Some textBox -> + // layer.AddAdornment(line.Extent, self, textBox) |> ignore + // do textBox.BeginAnimation(UIElement.OpacityProperty, da) + // | None -> () + // with + // | e -> Logging.Logging.logErrorf "Error occured: %A" e + // () + + with + | e -> Logging.Logging.logErrorf "Error occured: %A" e + () + } + Async.Start (asyncStuff , cancellationTokenSourceLayoutChanged.Token) () member __.Tags = new Concurrent.ConcurrentDictionary<_, _>() From 7ce7105c9fa4ae84c328e62fac30ac94b9359979 Mon Sep 17 00:00:00 2001 From: realvictorprm Date: Mon, 1 May 2017 15:50:28 +0200 Subject: [PATCH 5/6] More async, caching and some comments --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 225 +++++++++--------- 1 file changed, 117 insertions(+), 108 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index 6cf4acfef64..38ce70beae7 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -45,7 +45,9 @@ type internal CodeLensTagger let tagsChanged = new Event,SnapshotSpanEventArgs>() let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" - let mutable codeLensLines = ConcurrentDictionary() + let mutable codeLensResults = ConcurrentDictionary() + let mutable codeLensCacheAvailable = Generic.HashSet() + let mutable codeLensUIElementCache = ConcurrentDictionary() let mutable firstTimeChecked = false do assert (documentId <> null) @@ -61,12 +63,14 @@ type internal CodeLensTagger |> ClassificationTags.GetClassificationTypeName |> typeMap.Value.GetClassificationType |> formatMap.Value.GetTextProperties - + + let mutable visibleLines = Set.empty + let executeCodeLenseAsync () = let uiContext = SynchronizationContext.Current asyncMaybe { try - Async.Sleep(1000) |> Async.RunSynchronously + Async.Sleep(2000) |> Async.RunSynchronously Logging.Logging.logInfof "Rechecking code due to buffer edit!" let! document = workspace.CurrentSolution.GetDocument(documentId.Value) |> Option.ofObj let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) @@ -77,8 +81,11 @@ type internal CodeLensTagger let view = self.WpfTextView.Value; let textSnapshot = view.TextSnapshot.TextBuffer.CurrentSnapshot Logging.Logging.logInfof "Updating code lens due to buffer edit!" - codeLensLines.Clear(); - //let results = Concurrent.ConcurrentDictionary<_,_>() + + //Clear existing data and cache flags + codeLensResults.Clear() + codeLensCacheAvailable.Clear() + let useResults (displayContext: FSharpDisplayContext, func: FSharpMemberOrFunctionOrValue) = async { let lineNumber = Line.toZ func.DeclarationLocation.StartLine @@ -101,6 +108,16 @@ type internal CodeLensTagger Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore let navigation = QuickInfoNavigation(gotoDefinitionService, document, func.DeclarationLocation) + + if visibleLines.Contains lineNumber then + // The line might be visible but doesn't displaying the results yet, force it being redrawn. + visibleLines <- visibleLines.Remove(lineNumber) + + let line = textSnapshot.GetLineFromLineNumber(lineNumber) + // This is only used with a cached async so flag the data as ready to use + codeLensCacheAvailable.Add(line.Extent.GetText()) |> ignore + // Because the data is available notify that this line should be updated, displaying the results + tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) return Some taggedText, Some navigation | None -> return None, None else return None, None @@ -112,11 +129,9 @@ type internal CodeLensTagger | :? FSharpEntity as entity -> for func in entity.MembersFunctionsAndValues do let lineNumber = Line.toZ func.DeclarationLocation.StartLine - codeLensLines.[int lineNumber] <- Async.cache (useResults (symbolUse.DisplayContext, func)) - //let line = textSnapshot.GetLineFromLineNumber(lineNumber) - //tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) + let text = textSnapshot.GetLineFromLineNumber(int lineNumber).GetText() + codeLensResults.[text] <- Async.cache (useResults (symbolUse.DisplayContext, func)) | _ -> () - Logging.Logging.logInfof "Extraction complete, replacing old code lens data!" do! Async.SwitchToContext uiContext |> liftAsync let handler = tagsChanged; handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) @@ -133,41 +148,62 @@ type internal CodeLensTagger do! executeCodeLenseAsync() Async.Sleep(1000) |> Async.RunSynchronously } |> Async.Ignore |> Async.Start - + + /// Creates the code lens ui elements for the specified line + /// TODO, add caching to the UI elements let createCodeLensUIElementByLine (line:ITextViewLine) = - let view = self.WpfTextView.Value - let offset = - [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) - |> Option.defaultValue 0 - let realStart = line.Start.Add(offset) - let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) - let geometry = view.TextViewLines.GetMarkerGeometry(span) - if codeLensLines.ContainsKey(buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)) then - let (taggedTextOption, navigationOption) = codeLensLines.[buffer.CurrentSnapshot.GetLineNumberFromPosition(line.Start.Position)] |> Async.RunSynchronously - match taggedTextOption, navigationOption with - | Some taggedText, Some navigation -> - let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) - DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) - for text in taggedText do - let run = Documents.Run text.Text - DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) - let inl = - match text with - | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> - let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) - h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) - h :> Documents.Inline - | _ -> run :> _ - textBox.Inlines.Add inl - Canvas.SetLeft(textBox, geometry.Bounds.Left) - Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) - textBox.Opacity <- 0.5 - Some textBox - | _, _ -> None + let text = line.Extent.GetText() + if codeLensResults.ContainsKey(text) then // Check whether this line has code lens + if codeLensCacheAvailable.Contains(text) then // Also check whether cache is available + if codeLensUIElementCache.ContainsKey(text) then + // Use existing UI element which is proved to be safe to use + Some codeLensUIElementCache.[text] + else + // No delay because it's already computed + let taggedTextOption, navigationOption = codeLensResults.[text] |> Async.RunSynchronously + + let view = self.WpfTextView.Value + // Get the real offset so that the code lens are placed correctly + let offset = + [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) + |> Option.defaultValue 0 + let realStart = line.Start.Add(offset) + let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) + let geometry = view.TextViewLines.GetMarkerGeometry(span) + + // It could be that the options are empty so we need to check the results + match taggedTextOption, navigationOption with + | Some taggedText, Some navigation -> + // We have valid data, create our UI element + let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) + DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) + for text in taggedText do + let run = Documents.Run text.Text + DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) + let inl = + match text with + | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> + let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + textBox.Inlines.Add inl + Canvas.SetLeft(textBox, geometry.Bounds.Left) + Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) + textBox.Opacity <- 0.5 + codeLensUIElementCache.TryAdd(text, textBox) |> ignore + Some textBox + | _, _ -> None // There aren't any valid results with the current data, skip + else + // We start asynchrounus cache computation so that the UI isn't blocked + codeLensResults.[text] |> Async.Ignore |> Async.Start + // It's likely that if the cache isn't computed yet, that the current content + // of the UI element cache is wrong, so remove it. + let unusedElement = null + codeLensUIElementCache.TryRemove(text, &unusedElement) |> ignore + None //The cache wasn't available, skip else - None - - let mutable visibleLines = list.Empty + None // There aren't any code lens available for this line, skip do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) member __.BufferChanged ___ = @@ -180,8 +216,8 @@ type internal CodeLensTagger member val WpfTextView : Lazy = Lazy() with get,set member val CodeLensLayer = Lazy() with get, set - - member t.T = "" + + member __.TriggerTagsChanged args = tagsChanged.Trigger(self, args) member __.LayoutChanged (e:TextViewLayoutChangedEventArgs) = let uiContext = SynchronizationContext.Current @@ -192,48 +228,48 @@ type internal CodeLensTagger let asyncStuff = async{ if firstTimeChecked then - try - let s = async { - try - do! Async.SwitchToContext uiContext - let view = self.WpfTextView.Value - let buffer = view.TextBuffer.CurrentSnapshot - for line in e.NewOrReformattedLines do - if List.contains (buffer.GetLineNumberFromPosition(line.Start.Position)) visibleLines then - if not self.CodeLensLayer.IsValueCreated then - self.CodeLensLayer.Force() |> ignore - let layer = self.CodeLensLayer.Value - //let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) - let res = createCodeLensUIElementByLine line - match res with - | Some textBox -> - layer.AddAdornment(line.Extent, self, textBox) |> ignore - | None -> () - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - } - Async.Start (s, cancellationTokenSourceLayoutChanged.Token) - do! Async.Sleep(500) //Wait before we add it and check whether the lines are still valid (so visible) if not, nothing will happen + try // We always need to catch exceptions here because we're working in a critical section due to multi threading + let handleAlreadyVisible = + async { + do! Async.SwitchToContext uiContext + let view = self.WpfTextView.Value + let buffer = view.TextBuffer.CurrentSnapshot + for line in e.NewOrReformattedLines do + if Set.contains (buffer.GetLineNumberFromPosition(line.Start.Position)) visibleLines then + let tags = line.GetAdornmentTags self + let tagOption = tags |> Seq.tryHead + match tagOption with + | None -> () + | Some __ -> + let layer = self.CodeLensLayer.Value + let res = createCodeLensUIElementByLine line + match res with + | Some textBox -> + layer.AddAdornment(line.Extent, self, textBox) |> ignore + | None -> () + } + Async.Start (handleAlreadyVisible, cancellationTokenSourceLayoutChanged.Token) + // Wait before we add it and check whether the lines are still valid (so visible) + // This is important because we don't want lines being display instantly which causes worse performance. + do! Async.Sleep(500) do! Async.SwitchToContext uiContext let view = self.WpfTextView.Value + // We need a new snapshot, it could be already outdated + let buffer = view.TextBuffer.CurrentSnapshot let firstLine, lastLine = let firstLine, lastLine= (view.TextViewLines.FirstVisibleLine, view.TextViewLines.LastVisibleLine) - let buffer = view.TextBuffer.CurrentSnapshot buffer.GetLineNumberFromPosition(firstLine.Start.Position), buffer.GetLineNumberFromPosition(lastLine.Start.Position) - let buffer = view.TextBuffer.CurrentSnapshot - let realNewLines = e.NewOrReformattedLines |> Seq.filter (fun l -> not(List.contains (buffer.GetLineNumberFromPosition(l.Start.Position)) visibleLines)) + let lineAlreadyProcessed pos = not(Set.contains (buffer.GetLineNumberFromPosition(pos)) visibleLines) + let realNewLines = e.NewOrReformattedLines |> Seq.filter (fun line -> lineAlreadyProcessed line.Start.Position) for line in realNewLines do if line.IsValid then - Logging.Logging.logInfof "Redrawing line with number: %A" (view.TextBuffer.CurrentSnapshot.GetLineNumberFromPosition line.Start.Position) |> ignore let tags = line.GetAdornmentTags self let tagOption = tags |> Seq.tryHead match tagOption with | None -> () | Some __ -> try - if not self.CodeLensLayer.IsValueCreated then - self.CodeLensLayer.Force() |> ignore let layer = self.CodeLensLayer.Value let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(0.8))) let res = createCodeLensUIElementByLine line @@ -245,35 +281,7 @@ type internal CodeLensTagger with | e -> Logging.Logging.logErrorf "Error occured: %A" e () - visibleLines <- [ firstLine .. lastLine] - //let visibleLineNumbers = [ firstLine .. lastLine] |> List.filter (fun l -> not (visibleLines |> List.contains l)) - //Logging.Logging.logInfof "Content of visibleLines: %A" visibleLines - //let lines = visibleLineNumbers - // |> List.map view.TextBuffer.CurrentSnapshot.GetLineFromLineNumber - // |> List.map (fun l -> view.GetTextViewLineContainingBufferPosition l.Start) - //for line in lines do - // if line.IsValid then - // Logging.Logging.logInfof "Redrawing line with number: %A" (view.TextBuffer.CurrentSnapshot.GetLineNumberFromPosition line.Start.Position) |> ignore - // let tags = line.GetAdornmentTags self - // let tagOption = tags |> Seq.tryHead - // match tagOption with - // | None -> () - // | Some __ -> - // try - // if not self.CodeLensLayer.IsValueCreated then - // self.CodeLensLayer.Force() |> ignore - // let layer = self.CodeLensLayer.Value - // let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(2.))) - // let res = createCodeLensUIElementByLine line - // match res with - // | Some textBox -> - // layer.AddAdornment(line.Extent, self, textBox) |> ignore - // do textBox.BeginAnimation(UIElement.OpacityProperty, da) - // | None -> () - // with - // | e -> Logging.Logging.logErrorf "Error occured: %A" e - // () - + visibleLines <- Set [ firstLine .. lastLine] with | e -> Logging.Logging.logErrorf "Error occured: %A" e () @@ -285,7 +293,7 @@ type internal CodeLensTagger interface ITagger with [] - member this.TagsChanged = tagsChanged.Publish + member __.TagsChanged = tagsChanged.Publish override __.GetTags spans = seq{ @@ -294,9 +302,9 @@ type internal CodeLensTagger spans |> Seq.map (fun span -> span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive))) for tagSpan in translatedSpans do - let lineNumber = buffer.CurrentSnapshot.GetLineNumberFromPosition(tagSpan.Start.Position) - let codeLens = codeLensLines - if codeLens.ContainsKey(lineNumber) then + let line = buffer.CurrentSnapshot.GetLineFromPosition(tagSpan.Start.Position) + let codeLens = codeLensResults + if codeLens.ContainsKey(line.GetText()) then let res = TagSpan(tagSpan, CodeLensTag(0., 15., 0., 0., 0., PositionAffinity.Predecessor, self, self)):> ITagSpan yield res } @@ -340,8 +348,6 @@ type internal CodeLensProvider CodeLens.Add((buffer, provider)) provider - - [); Name("CodeLens"); Order(Before = PredefinedAdornmentLayers.Text); TextViewRole(PredefinedTextViewRoles.Document)>] @@ -351,9 +357,12 @@ type internal CodeLensProvider override __.TextViewCreated view = let tagger = getSuitableAdornmentProvider view.TextBuffer tagger.WpfTextView <- Lazy<_>.CreateFromValue view + tagger.WpfTextView.Force() |> ignore tagger.CodeLensLayer <- Lazy<_>.CreateFromValue(view.GetAdornmentLayer "CodeLens") + tagger.CodeLensLayer.Force() |> ignore view.LayoutChanged.AddHandler(fun _ e -> tagger.LayoutChanged e) - view.DisplayTextLineContainingBufferPosition(view.TextViewLines.FirstVisibleLine.Start, 0., ViewRelativePosition.Top) + // The view has been initialized. Notify that we can now theoretically display CodeLens + tagger.TriggerTagsChanged (SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) () interface ITaggerProvider with From 3e22dd0837779014652d2ac37af2c120ebe4ac02 Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Mon, 1 May 2017 18:47:56 +0300 Subject: [PATCH 6/6] refactor code lens --- .../src/FSharp.Editor/CodeLens/CodeLens.fs | 529 ++++++++---------- .../src/FSharp.Editor/Common/Extensions.fs | 8 +- 2 files changed, 247 insertions(+), 290 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs index 38ce70beae7..9541bdbe0f9 100644 --- a/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs +++ b/vsintegration/src/FSharp.Editor/CodeLens/CodeLens.fs @@ -28,10 +28,15 @@ open Microsoft.VisualStudio.Text.Tagging open System.Collections.Concurrent open System.Collections open System.Windows.Media.Animation - +open Microsoft.VisualStudio.FSharp.Editor.Logging + type CodeLensTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag:obj, providerTag:obj) = inherit SpaceNegotiatingAdornmentTag(width, topSpace, baseline, textHeight, bottomSpace, affinity, tag, providerTag) +type internal CodeLens = + { TaggedText: Async<(ResizeArray * QuickInfoNavigation) option> + Shown: bool } + type internal CodeLensTagger ( workspace: Workspace, @@ -42,280 +47,241 @@ type internal CodeLensTagger typeMap: Lazy, gotoDefinitionService: FSharpGoToDefinitionService ) as self = - let tagsChanged = new Event,SnapshotSpanEventArgs>() - - let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" - let mutable codeLensResults = ConcurrentDictionary() - let mutable codeLensCacheAvailable = Generic.HashSet() - let mutable codeLensUIElementCache = ConcurrentDictionary() - let mutable firstTimeChecked = false - - do assert (documentId <> null) - - let mutable cancellationTokenSourceBufferChanged = new CancellationTokenSource() - let mutable cancellationTokenBufferChanged = cancellationTokenSourceBufferChanged.Token - - let mutable cancellationTokenSourceLayoutChanged = new CancellationTokenSource() - let layoutTagToFormatting (layoutTag: LayoutTag) = - layoutTag - |> RoslynHelpers.roslynTag - |> ClassificationTags.GetClassificationTypeName - |> typeMap.Value.GetClassificationType - |> formatMap.Value.GetTextProperties - - let mutable visibleLines = Set.empty - - let executeCodeLenseAsync () = - let uiContext = SynchronizationContext.Current - asyncMaybe { - try - Async.Sleep(2000) |> Async.RunSynchronously - Logging.Logging.logInfof "Rechecking code due to buffer edit!" - let! document = workspace.CurrentSolution.GetDocument(documentId.Value) |> Option.ofObj - let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) - let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) - Logging.Logging.logInfof "Getting uses of all symbols!" - let! symbolUses = checkFileResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync - - let view = self.WpfTextView.Value; - let textSnapshot = view.TextSnapshot.TextBuffer.CurrentSnapshot - Logging.Logging.logInfof "Updating code lens due to buffer edit!" - - //Clear existing data and cache flags - codeLensResults.Clear() - codeLensCacheAvailable.Clear() + let tagsChanged = new Event,SnapshotSpanEventArgs>() + let formatMap = lazy typeMap.Value.ClassificationFormatMapService.GetClassificationFormatMap "tooltip" + let codeLensResults = ConcurrentDictionary() + let codeLensUIElementCache = ConcurrentDictionary() + let visibleLines = ConcurrentDictionary() + let mutable firstTimeChecked = false + let mutable bufferChangedCts = new CancellationTokenSource() + let mutable layoutChangedCts = new CancellationTokenSource() + let mutable view: IWpfTextView option = None + let mutable codeLensLayer: IAdornmentLayer option = None + + let layoutTagToFormatting (layoutTag: LayoutTag) = + layoutTag + |> RoslynHelpers.roslynTag + |> ClassificationTags.GetClassificationTypeName + |> typeMap.Value.GetClassificationType + |> formatMap.Value.GetTextProperties + + let executeCodeLenseAsync () = + let uiContext = SynchronizationContext.Current + asyncMaybe { + let! view = view + do! Async.Sleep 500 |> liftAsync + logInfof "Rechecking code due to buffer edit!" + let! document = workspace.CurrentSolution.GetDocument(documentId.Value) |> Option.ofObj + let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) + let! _, _, checkFileResults = checker.ParseAndCheckDocument(document, options, allowStaleResults = true) + logInfof "Getting uses of all symbols!" + let! symbolUses = checkFileResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync + let textSnapshot = view.TextSnapshot.TextBuffer.CurrentSnapshot + logInfof "Updating code lens due to buffer edit!" + + // Clear existing data and cache flags + codeLensResults.Clear() + + let useResults (displayContext: FSharpDisplayContext, func: FSharpMemberOrFunctionOrValue, lineStr: string) = + async { + let lineNumber = Line.toZ func.DeclarationLocation.StartLine + + if (lineNumber >= 0 || lineNumber < textSnapshot.LineCount) && + not func.IsPropertyGetterMethod && + not func.IsPropertySetterMethod then + + match func.FullTypeSafe with + | Some ty -> + let! displayEnv = checkFileResults.GetDisplayEnvForPos(func.DeclarationLocation.Start) + + let displayContext = + match displayEnv with + | Some denv -> FSharpDisplayContext(fun _ -> denv) + | None -> displayContext + + let typeLayout = ty.FormatLayout(displayContext) + let taggedText = ResizeArray() + Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore + let navigation = QuickInfoNavigation(gotoDefinitionService, document, func.DeclarationLocation) + + // The line might be visible but doesn't displaying the results yet, force it being redrawn. + visibleLines.TryRemove lineNumber |> ignore + let line = textSnapshot.GetLineFromLineNumber(lineNumber) + // This is only used with a cached async so flag the data as ready to use + codeLensResults.[lineStr] <- { codeLensResults.[lineStr] with Shown = true } + // Because the data is available notify that this line should be updated, displaying the results + tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) + return Some (taggedText, navigation) + | None -> return None + else return None + } - let useResults (displayContext: FSharpDisplayContext, func: FSharpMemberOrFunctionOrValue) = - async { + for symbolUse in symbolUses do + if symbolUse.IsFromDefinition then + match symbolUse.Symbol with + | :? FSharpEntity as entity -> + for func in entity.MembersFunctionsAndValues do let lineNumber = Line.toZ func.DeclarationLocation.StartLine - - if (lineNumber >= 0 || lineNumber < textSnapshot.LineCount) && - not func.IsPropertyGetterMethod && - not func.IsPropertySetterMethod then - - match func.FullTypeSafe with - | Some ty -> - let! displayEnv = checkFileResults.GetDisplayEnvForPos(func.DeclarationLocation.Start) - - let displayContext = - match displayEnv with - | Some denv -> FSharpDisplayContext(fun _ -> denv) - | None -> displayContext - - let typeLayout = ty.FormatLayout(displayContext) - let taggedText = ResizeArray() - Layout.renderL (Layout.taggedTextListR taggedText.Add) typeLayout |> ignore - - let navigation = QuickInfoNavigation(gotoDefinitionService, document, func.DeclarationLocation) - - if visibleLines.Contains lineNumber then - // The line might be visible but doesn't displaying the results yet, force it being redrawn. - visibleLines <- visibleLines.Remove(lineNumber) - - let line = textSnapshot.GetLineFromLineNumber(lineNumber) - // This is only used with a cached async so flag the data as ready to use - codeLensCacheAvailable.Add(line.Extent.GetText()) |> ignore - // Because the data is available notify that this line should be updated, displaying the results - tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(line.Start, line.End))) - return Some taggedText, Some navigation - | None -> return None, None - else return None, None - } - - for symbolUse in symbolUses do - if symbolUse.IsFromDefinition then - match symbolUse.Symbol with - | :? FSharpEntity as entity -> - for func in entity.MembersFunctionsAndValues do - let lineNumber = Line.toZ func.DeclarationLocation.StartLine - let text = textSnapshot.GetLineFromLineNumber(int lineNumber).GetText() - codeLensResults.[text] <- Async.cache (useResults (symbolUse.DisplayContext, func)) - | _ -> () - do! Async.SwitchToContext uiContext |> liftAsync - let handler = tagsChanged; - handler.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) - Logging.Logging.logInfof "Finished updating code lens." |> ignore - if not firstTimeChecked then - firstTimeChecked <- true - with - | ex -> Logging.Logging.logErrorf "Error occured: %A" ex - - } - - do asyncMaybe{ - while not firstTimeChecked do - do! executeCodeLenseAsync() - Async.Sleep(1000) |> Async.RunSynchronously - } |> Async.Ignore |> Async.Start - - /// Creates the code lens ui elements for the specified line - /// TODO, add caching to the UI elements - let createCodeLensUIElementByLine (line:ITextViewLine) = - let text = line.Extent.GetText() - if codeLensResults.ContainsKey(text) then // Check whether this line has code lens - if codeLensCacheAvailable.Contains(text) then // Also check whether cache is available - if codeLensUIElementCache.ContainsKey(text) then - // Use existing UI element which is proved to be safe to use - Some codeLensUIElementCache.[text] - else - // No delay because it's already computed - let taggedTextOption, navigationOption = codeLensResults.[text] |> Async.RunSynchronously - - let view = self.WpfTextView.Value - // Get the real offset so that the code lens are placed correctly - let offset = - [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) - |> Option.defaultValue 0 - let realStart = line.Start.Add(offset) - let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) - let geometry = view.TextViewLines.GetMarkerGeometry(span) - - // It could be that the options are empty so we need to check the results - match taggedTextOption, navigationOption with - | Some taggedText, Some navigation -> - // We have valid data, create our UI element - let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) - DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) - for text in taggedText do - let run = Documents.Run text.Text - DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) - let inl = - match text with - | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> - let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) - h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) - h :> Documents.Inline - | _ -> run :> _ - textBox.Inlines.Add inl - Canvas.SetLeft(textBox, geometry.Bounds.Left) - Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) - textBox.Opacity <- 0.5 - codeLensUIElementCache.TryAdd(text, textBox) |> ignore - Some textBox - | _, _ -> None // There aren't any valid results with the current data, skip - else - // We start asynchrounus cache computation so that the UI isn't blocked - codeLensResults.[text] |> Async.Ignore |> Async.Start - // It's likely that if the cache isn't computed yet, that the current content - // of the UI element cache is wrong, so remove it. - let unusedElement = null - codeLensUIElementCache.TryRemove(text, &unusedElement) |> ignore - None //The cache wasn't available, skip + let text = textSnapshot.GetLineFromLineNumber(int lineNumber).GetText() + + codeLensResults.[text] <- + { TaggedText = Async.cache (useResults (symbolUse.DisplayContext, func, text)) + Shown = false } + | _ -> () + + do! Async.SwitchToContext uiContext |> liftAsync + tagsChanged.Trigger(self, SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) + logInfof "Finished updating code lens." + + if not firstTimeChecked then + firstTimeChecked <- true + } |> Async.Ignore + + do async { + while not firstTimeChecked do + do! executeCodeLenseAsync() + do! Async.Sleep(1000) + } |> Async.Start + + /// Creates the code lens ui elements for the specified line + /// TODO, add caching to the UI elements + let createCodeLensUIElementByLine (line: ITextViewLine) = + let text = line.Extent.GetText() + asyncMaybe { + if codeLensUIElementCache.ContainsKey(text) then + // Use existing UI element which is proved to be safe to use + return codeLensUIElementCache.[text] else - None // There aren't any code lens available for this line, skip - - do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) - member __.BufferChanged ___ = - cancellationTokenSourceBufferChanged.Cancel() // Stop all ongoing async workflow. - cancellationTokenSourceBufferChanged.Dispose() - cancellationTokenSourceBufferChanged <- new CancellationTokenSource() - cancellationTokenBufferChanged <- cancellationTokenSourceBufferChanged.Token - executeCodeLenseAsync () |> Async.Ignore |> RoslynHelpers.StartAsyncSafe cancellationTokenBufferChanged + let! lens = codeLensResults.TryFind(text) // Check whether this line has code lens + // No delay because it's already computed + let! taggedText, navigation = lens.TaggedText + let! view = view + // Get the real offset so that the code lens are placed correctly + let offset = + [0..line.Length - 1] |> Seq.tryFind (fun i -> not (Char.IsWhiteSpace (line.Start.Add(i).GetChar()))) + |> Option.defaultValue 0 + + let realStart = line.Start.Add(offset) + let span = SnapshotSpan(line.Snapshot, Span.FromBounds(int realStart, int line.End)) + let geometry = view.TextViewLines.GetMarkerGeometry(span) + let textBox = TextBlock(Width = view.ViewportWidth, Background = Brushes.Transparent, Opacity = 0.5, TextTrimming = TextTrimming.WordEllipsis) + DependencyObjectExtensions.SetDefaultTextProperties(textBox, formatMap.Value) - member val WpfTextView : Lazy = Lazy() with get,set - - member val CodeLensLayer = Lazy() with get, set - - member __.TriggerTagsChanged args = tagsChanged.Trigger(self, args) - - member __.LayoutChanged (e:TextViewLayoutChangedEventArgs) = - let uiContext = SynchronizationContext.Current - - cancellationTokenSourceLayoutChanged.Cancel() - cancellationTokenSourceLayoutChanged.Dispose() - cancellationTokenSourceLayoutChanged <- new CancellationTokenSource() - let asyncStuff = - async{ - if firstTimeChecked then - try // We always need to catch exceptions here because we're working in a critical section due to multi threading - let handleAlreadyVisible = - async { - do! Async.SwitchToContext uiContext - let view = self.WpfTextView.Value - let buffer = view.TextBuffer.CurrentSnapshot - for line in e.NewOrReformattedLines do - if Set.contains (buffer.GetLineNumberFromPosition(line.Start.Position)) visibleLines then - let tags = line.GetAdornmentTags self - let tagOption = tags |> Seq.tryHead - match tagOption with - | None -> () - | Some __ -> - let layer = self.CodeLensLayer.Value - let res = createCodeLensUIElementByLine line - match res with - | Some textBox -> - layer.AddAdornment(line.Extent, self, textBox) |> ignore - | None -> () - } - Async.Start (handleAlreadyVisible, cancellationTokenSourceLayoutChanged.Token) - // Wait before we add it and check whether the lines are still valid (so visible) - // This is important because we don't want lines being display instantly which causes worse performance. - do! Async.Sleep(500) - do! Async.SwitchToContext uiContext - let view = self.WpfTextView.Value - // We need a new snapshot, it could be already outdated - let buffer = view.TextBuffer.CurrentSnapshot - let firstLine, lastLine = - let firstLine, lastLine= (view.TextViewLines.FirstVisibleLine, view.TextViewLines.LastVisibleLine) - buffer.GetLineNumberFromPosition(firstLine.Start.Position), - buffer.GetLineNumberFromPosition(lastLine.Start.Position) - let lineAlreadyProcessed pos = not(Set.contains (buffer.GetLineNumberFromPosition(pos)) visibleLines) - let realNewLines = e.NewOrReformattedLines |> Seq.filter (fun line -> lineAlreadyProcessed line.Start.Position) - for line in realNewLines do - if line.IsValid then - let tags = line.GetAdornmentTags self - let tagOption = tags |> Seq.tryHead - match tagOption with - | None -> () - | Some __ -> - try - let layer = self.CodeLensLayer.Value - let da = new DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = new Duration(TimeSpan.FromSeconds(0.8))) - let res = createCodeLensUIElementByLine line - match res with - | Some textBox -> - layer.AddAdornment(line.Extent, self, textBox) |> ignore - textBox.BeginAnimation(UIElement.OpacityProperty, da) - | None -> () - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () - visibleLines <- Set [ firstLine .. lastLine] - with - | e -> Logging.Logging.logErrorf "Error occured: %A" e - () - } - Async.Start (asyncStuff , cancellationTokenSourceLayoutChanged.Token) - () - - member __.Tags = new Concurrent.ConcurrentDictionary<_, _>() - - interface ITagger with - [] - member __.TagsChanged = tagsChanged.Publish - - override __.GetTags spans = - seq{ - let translatedSpans = - new NormalizedSnapshotSpanCollection( - spans - |> Seq.map (fun span -> span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive))) - for tagSpan in translatedSpans do - let line = buffer.CurrentSnapshot.GetLineFromPosition(tagSpan.Start.Position) - let codeLens = codeLensResults - if codeLens.ContainsKey(line.GetText()) then - let res = TagSpan(tagSpan, CodeLensTag(0., 15., 0., 0., 0., PositionAffinity.Predecessor, self, self)):> ITagSpan - yield res - } + for text in taggedText do + let run = Documents.Run text.Text + DependencyObjectExtensions.SetTextProperties (run, layoutTagToFormatting text.Tag) + let inl = + match text with + | :? Layout.NavigableTaggedText as nav when navigation.IsTargetValid nav.Range -> + let h = Documents.Hyperlink(run, ToolTip = nav.Range.FileName) + h.Click.Add (fun _ -> navigation.NavigateTo nav.Range) + h :> Documents.Inline + | _ -> run :> _ + textBox.Inlines.Add inl + + Canvas.SetLeft(textBox, geometry.Bounds.Left) + Canvas.SetTop(textBox, geometry.Bounds.Top - 15.) + textBox.Opacity <- 0.5 + return textBox + } |> Async.map (fun ui -> + match ui with + | Some ui -> codeLensUIElementCache.TryAdd(text, ui) |> ignore + | None -> + let unusedElement = null + codeLensUIElementCache.TryRemove(text, &unusedElement) |> ignore + ui) + + do buffer.Changed.AddHandler(fun _ e -> (self.BufferChanged e)) + + member __.BufferChanged ___ = + bufferChangedCts.Cancel() // Stop all ongoing async workflow. + bufferChangedCts.Dispose() + bufferChangedCts <- new CancellationTokenSource() + executeCodeLenseAsync () |> Async.Ignore |> RoslynHelpers.StartAsyncSafe bufferChangedCts.Token + + member __.SetView value = + view <- Some value + codeLensLayer <- Some (value.GetAdornmentLayer "CodeLens") + + member __.TriggerTagsChanged args = tagsChanged.Trigger(self, args) + + member this.LayoutChanged (e:TextViewLayoutChangedEventArgs) = + let uiContext = SynchronizationContext.Current + layoutChangedCts.Cancel() + layoutChangedCts.Dispose() + layoutChangedCts <- new CancellationTokenSource() + + asyncMaybe { + //if firstTimeChecked then + do! Async.SwitchToContext uiContext |> liftAsync + let! view = view + let! layer = codeLensLayer + let buffer = view.TextBuffer.CurrentSnapshot + + for line in e.NewOrReformattedLines do + if visibleLines.ContainsKey (buffer.GetLineNumberFromPosition(line.Start.Position)) then + if not (Seq.isEmpty (line.GetAdornmentTags this)) then + let! res = createCodeLensUIElementByLine line |> liftAsync + match res with + | Some textBox -> layer.AddAdornment(line.Extent, this, textBox) |> ignore + | None -> () + + // Wait before we add it and check whether the lines are still valid (so visible) + // This is important because we don't want lines being display instantly which causes worse performance. + do! Async.Sleep(500) |> liftAsync + // We need a new snapshot, it could be already outdated + let buffer = view.TextBuffer.CurrentSnapshot + + let firstLine, lastLine = + let firstLine, lastLine= (view.TextViewLines.FirstVisibleLine, view.TextViewLines.LastVisibleLine) + buffer.GetLineNumberFromPosition(firstLine.Start.Position), + buffer.GetLineNumberFromPosition(lastLine.Start.Position) + let lineAlreadyProcessed pos = not (visibleLines.ContainsKey (buffer.GetLineNumberFromPosition(pos))) + let realNewLines = e.NewOrReformattedLines |> Seq.filter (fun line -> lineAlreadyProcessed line.Start.Position) + + for line in realNewLines do + if line.IsValid then + if not (Seq.isEmpty (line.GetAdornmentTags this)) then + let da = DoubleAnimation(From = Nullable 0., To = Nullable 0.5, Duration = Duration(TimeSpan.FromSeconds 0.8)) + let! res = createCodeLensUIElementByLine line |> liftAsync + match res with + | Some textBox -> + layer.AddAdornment(line.Extent, this, textBox) |> ignore + textBox.BeginAnimation(UIElement.OpacityProperty, da) + | None -> () + + for line in firstLine..lastLine do + visibleLines.[line] <- () + } + |> Async.Ignore + |> RoslynHelpers.StartAsyncSafe layoutChangedCts.Token + + member __.Tags = ConcurrentDictionary() + + interface ITagger with + [] + member __.TagsChanged = tagsChanged.Publish + + override this.GetTags spans = + seq { + let translatedSpans = + spans + |> Seq.map (fun span -> span.TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeExclusive)) + |> NormalizedSnapshotSpanCollection + + for tagSpan in translatedSpans do + let line = buffer.CurrentSnapshot.GetLineFromPosition(tagSpan.Start.Position) + if codeLensResults.ContainsKey(line.GetText()) then + yield TagSpan(tagSpan, CodeLensTag(0., 15., 0., 0., 0., PositionAffinity.Predecessor, this, this)) :> ITagSpan + } [)>] [)>] [)>] [] [] - type internal CodeLensProvider [] ( @@ -325,18 +291,19 @@ type internal CodeLensProvider typeMap: Lazy, gotoDefinitionService: FSharpGoToDefinitionService ) as __ = - let CodeLens = ResizeArray<_ * _>() + + let taggers = ResizeArray() let componentModel = Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel let workspace = componentModel.GetService() /// Returns an provider for the textView if already one has been created. Else create one. let getSuitableAdornmentProvider (buffer) = - let res = CodeLens |> Seq.tryFind(fun (view, _) -> view = buffer) + let res = taggers |> Seq.tryFind(fun (view, _) -> view = buffer) match res with | Some (_, res) -> res | None -> let documentId = - lazy( + lazy ( match textDocumentFactory.TryGetTextDocument(buffer) with | true, textDocument -> Seq.tryHead (workspace.CurrentSolution.GetDocumentIdsWithFilePath(textDocument.FilePath)) @@ -345,21 +312,19 @@ type internal CodeLensProvider ) let provider = CodeLensTagger(workspace, documentId, buffer, checkerProvider.Checker, projectInfoManager, typeMap, gotoDefinitionService) - CodeLens.Add((buffer, provider)) + taggers.Add((buffer, provider)) provider - [); Name("CodeLens"); - Order(Before = PredefinedAdornmentLayers.Text); - TextViewRole(PredefinedTextViewRoles.Document)>] + [)>] + [] + [] + [] member val CodeLensAdornmentLayerDefinition : AdornmentLayerDefinition = null with get, set interface IWpfTextViewCreationListener with override __.TextViewCreated view = let tagger = getSuitableAdornmentProvider view.TextBuffer - tagger.WpfTextView <- Lazy<_>.CreateFromValue view - tagger.WpfTextView.Force() |> ignore - tagger.CodeLensLayer <- Lazy<_>.CreateFromValue(view.GetAdornmentLayer "CodeLens") - tagger.CodeLensLayer.Force() |> ignore + tagger.SetView view view.LayoutChanged.AddHandler(fun _ e -> tagger.LayoutChanged e) // The view has been initialized. Notify that we can now theoretically display CodeLens tagger.TriggerTagsChanged (SnapshotSpanEventArgs(SnapshotSpan(view.TextViewLines.FirstVisibleLine.Start, view.TextViewLines.LastVisibleLine.End))) @@ -368,18 +333,4 @@ type internal CodeLensProvider interface ITaggerProvider with override __.CreateTagger(buffer) = let tagger = getSuitableAdornmentProvider buffer - box (tagger) :?> _ - -module Test = - let a = "a" - -//module Test = -// let t = "" - - -// type Cherry (s, a, b) = -// let s = s -// let b = b -// let a = a - -// let s = "" \ No newline at end of file + box (tagger) :?> _ \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index cfd7d131177..f9d72cb898d 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -199,4 +199,10 @@ module Array = /// Returns true if one array has trailing elements equal to another's. let endsWith (suffix: _ []) (whole: _ []) = - isSubArray suffix whole (whole.Length-suffix.Length) \ No newline at end of file + isSubArray suffix whole (whole.Length-suffix.Length) + +type System.Collections.Concurrent.ConcurrentDictionary<'k, 'v> with + member this.TryFind (key: 'k) : 'v option = + match this.TryGetValue key with + | true, v -> Some v + | _ -> None \ No newline at end of file