Skip to content

Conversation

@majocha
Copy link
Contributor

@majocha majocha commented Mar 10, 2017

Reading unfamiliar code with scarce type annotations I often wish I could click the types in quickinfo to go directly to some definition.
quickinfo2

I put together a proof of concept. The code is hacky, usability is not that good, but it's a start and shows that a lot is possible :)

quickinfo3

EDIT
Changed links to show when mouse enters the QuickInfo.
Linked(shared among many projects) files also are handled.
Added small tooltip showing file path on hover over link.

@msftclas
Copy link

@majocha,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by Microsoft. We will now review your pull request.
Thanks,
Microsoft Pull Request Bot

@smoothdeveloper
Copy link
Contributor

That is totally awesome :)

@vasily-kirichenko
Copy link
Contributor

OMG it's awesome! I've seen such functionality only once, in Nemerle VS integration.

@vasily-kirichenko
Copy link
Contributor

Is it possible to make Quick Info hierarchical? I mean if you hover over a symbol on a tooltip, another tooltip appears and so on.

@smoothdeveloper
Copy link
Contributor

In future we could have right click / find usages as well?

| Some pathText -> pathText +.+ nmF xref

let tagEntityRefName (xref: EntityRef) name =
let rng = Some ( box xref.DefinitionRange )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need to box here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, because it's a hack. extra info passed around is just obj option because I initially wasn't sure what's needed to navigate.

@majocha
Copy link
Contributor Author

majocha commented Mar 10, 2017

@vasily-kirichenko I guess it is. It's all just a WPF FrameworkElement. With some work put in sky is the limit.

| Delegate of string
| Enum of string
| Event of string
| Alias of obj option * string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put range here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it is defined and accessible yet here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's defined later. Maybe introduce a new type just for this? I feel myself (very) uncomfortable looking at type erasure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Even if you need the hack you can use type BoxRange = BoxRange of obj and then it's very easy to check the casts in/out of the type are valid

@vasily-kirichenko
Copy link
Contributor

You committed ValudTuple.dll again :)

@majocha
Copy link
Contributor Author

majocha commented Mar 10, 2017

@vasily-kirichenko dang it!

module TaggedTextOps =
#endif
let tagAlias = TaggedText.Alias
let private blank name = None, name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for private here because there it has signature file.

val tagUnionCase : (string -> TaggedText)
val tagDelegate : (string -> TaggedText)
val tagEnum : (string -> TaggedText)
val tagEvent : (string -> TaggedText)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these parens? Whether add them to all the functions or remove everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were needed for some reason. Something about function values...

open System.Windows
open Microsoft.CodeAnalysis.Editor
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
open Microsoft.CodeAnalysis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the last is unused.

for NavigableRoslynText(tag, text, xopt) in content do
let rangeOpt =
match xopt with
| Some xref ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xopt and xref are very puzzling names.

@majocha
Copy link
Contributor Author

majocha commented Mar 10, 2017

@vasily-kirichenko thanks for the review. I'll clean it up and work some more soon.

And I need to find a way to get rid of ValueTuple :)


let h = Documents.Hyperlink(Documents.Run(text))
h.Click.Add <| fun _ ->
Logging.Logging.logInfof "click! %A" range
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove logging, please. It will appear on VS output pane even if vsix is built in release configuration.

let documentNavigationService =workspace.Services.GetService<IDocumentNavigationService>()
let filePath = System.IO.Path.GetFullPathSafe range.FileName
match workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) |> List.ofSeq with
| id :: _ ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If file is linked to several projects, it's possible to jump to the wrong document. We need to find a document for which FSharpSymbol.Assembly.SimpleName = document.Project.AssemblyName.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that we have to pass assembly name alongside with range.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is tricky. It seems workspace will not open another editor if we already have a file opened in IDE from the wrong project. But you're right, main concern is to not open from wrong project during navigation.
Range doesn't carry assembly info but we have the Symbol because we're in the context of quickinfo, so it's doable.

let tagEvent = TaggedText.Event

let tagClass name = if Set.contains name keywordTypes then TaggedText.Keyword name else TaggedText.Class(None, name)
let tagUnionCase = blank >> TaggedText.UnionCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to avoid the parentheses in the signature file then just use an explicit argument rather than >>

| Some xref ->
match xref with
| :? Microsoft.FSharp.Compiler.Range.range as range
when range.FileName <> "startup" ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make "startup" a named constant in the compiler and replace all its occurrences in the compiler codebase with a reference to that constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already is one in defined in TcGlogals.fs :) but I use a value Range.rangeStartup instead of the string now.

@vasily-kirichenko
Copy link
Contributor

@majocha I think this PR is ready? Remove WIP from the title, please.

@cloudRoutine
Copy link
Contributor

@vasily-kirichenko I don't think it's worthwhile to revert it as the default behavior, adding an option to disable it is plenty.

@majocha
Copy link
Contributor Author

majocha commented Mar 15, 2017

@Pilchie this PR uses Roslyn implementation of QuickInfo controller, only rendering is augmented. Tooltip should show up and disappear exactly as it normally does.

@majocha majocha changed the title [WIP] Clickable quickinfo Clickable QuickInfo with "go to type" Mar 15, 2017
@majocha
Copy link
Contributor Author

majocha commented Mar 15, 2017

@Pilchie to address your first question, about ctrl+click extension:
"Go to definition" is not always helpful in F#, where return type and parameter types of a function are often inferred.
When there is a type annotation it would be a matter of two clicks: go to definition, go to definition on a type. But I was often getting stuck browsing F# code when there was no type annotation at the definition.

@olegtk
Copy link

olegtk commented Mar 15, 2017

The editor also supports advanced interactive QuickInfo popup if you want more control over its behavior. This is how "Show potential fixes" link and floating LightBulb in QuickInfo over an error squiggle are implemented.
https://msdn.microsoft.com/en-us/library/microsoft.visualstudio.language.intellisense.iinteractivequickinfocontent.aspx

| Some(range) when canGoto range ->
let h = Documents.Hyperlink(Documents.Run(text), ToolTip = range.FileName)
h.Click.Add <| fun _ ->
Forms.SendKeys.Send("{ESC}") //TODO: Dismiss QuickInfo properly, maybe using IQuickInfoSession.Dismiss()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we fix this hack before merging?

open ReflectionAdapters
#endif

[<NoEquality; NoComparison>]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still makes me really uneasy. I don't know what @KevinRansom and @dsyme think of this - surely there's a way we can sort this out without having to resort to 'obj'. We're just completely subverting the type system here and saying 'trust me I know what I'm doing'.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is fixable, by moving range file on it's own (if it doesn't contain too much stuff as well), maybe try a PR on this PR which just moves the files in the relevant fsprojs if that's doable.

I think the hack is fine (looking at unboxRange and its usage) but it needs an explicit comment where the type is defined as well as in unboxRange, and having a statement (beside the picture from @vasily-kirichenko) that this move was attempted but seemed disruptive would be good too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I understand the rationale, but it's easy to move the file to be on its own. There's no need for us to hack around like this. Just move the file up to the top of the project - like you said it's only a small type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saul I'm ok with moving the file unless it proves very difficult, it's totally fair to push back on this kind of hack.

This file is a bit odd because it also forms part of FSHarp.Core - we should probably just duplicate it out so that the compiler and FSHarp.Core are independent code bases, and we don't have all the whacky #if COMPILER

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saul (I've done this kind of hack in the past but should have been scolded at the time!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I can't do this one without help. I admit what I did here with the #if directives and attributes is a copy paste job without fully understanding what's going on.

@vasily-kirichenko
Copy link
Contributor

It works great. However, I find showing full file path in a link tooltip a bit useless. What about showing full name instead? Or full name + file name, like

My.Ns.M1.foo
c:\path\types.fs

?

@majocha
Copy link
Contributor Author

majocha commented Mar 16, 2017

@vasily-kirichenko Doable. I think it is a matter of passing more useful info from AST than just the definition range? Alternatively, a lot more info can be fetched and filled in the background, up to getting another quickinfo and making it recursive. Maybe ctrl-click on a link could enter such "drill-down" mode? I'll experiment as soon as I can.

@vasily-kirichenko
Copy link
Contributor

@KevinRansom any reason why this awesome PR still has not been merged?

@majocha
Copy link
Contributor Author

majocha commented Mar 20, 2017

According to this
http://source.roslyn.io/#Microsoft.VisualStudio.LanguageServices/Implementation/Workspace/VisualStudioDocumentNavigationService.cs,125
Roslyn already handles linked files in TryNavigateToSpan, switching to correct DocumentId as needed.
That means we don't need to do it again. It looks like it still does work. Edit: Nope.
(Navigation Bar often shows wrong project but this seems unrelated? Edit: There is different behavior than in C# projects. Trying to select already opened linked file from different project, selection in Solution Explorer will jump around, instead of just seamlessly switching project in the NavBar. This is for another issue, I guess.

@KevinRansom
Copy link
Contributor

@vasily-kirichenko ... I hadn't got around to re-reviewing it yet

@KevinRansom
Copy link
Contributor

@majocha

Thank you for this PR.

Kevin

@KevinRansom KevinRansom merged commit 13abe6f into dotnet:master Mar 20, 2017
@cloudRoutine
Copy link
Contributor

@majocha I've noticed that the links in the tooltips seem to favor the .fsi files over the .fs files. I think it would be good if tooltips in .fs files had links to other .fs and tooltips in .fsi files linking to other .fsi

@cartermp
Copy link
Contributor

@cloudRoutine Could you create an issue?

@cloudRoutine
Copy link
Contributor

sure, I've got some other ideas for functionality around this feature so I'll put it all together there

@smoothdeveloper
Copy link
Contributor

This feature rocks, @majocha should it also enable clicking on the symbol itself (when not at the definition)?

I know we might support ctrl+click later, but when using the mouse to get the tooltip, it also makes sense to go to the definition on the symbol itself.

image

@majocha
Copy link
Contributor Author

majocha commented Mar 21, 2017

@smoothdeveloper Makes sense! @cloudRoutine I think a discussion issue would be great, where we could talk ideas, design and also implementation and integration of stuff.

@majocha majocha deleted the navigable branch March 27, 2017 19:08
nosami pushed a commit to xamarin/visualfsharp that referenced this pull request Jan 26, 2022
* custom tooltip test

* tagClass augmented

* kinda works

* getting there

* module, alias

* not sure if works

* fix build

* go away!

* parens out

* halleluyah it builds

* blank space on empty doc removed

* more sensible

* handle linked files

* collapse empty TextBlocks

* styling, simple tooltip, handle scripts

* dismiss quickinfo on navigation

* reliably dismiss quickinfo session

* interface instead of boxing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.