My favorite F# code I've written
source link: https://phillipcarter.dev/posts/favorite-fsharp-code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
My favorite F# code I've written
Dec 25, 2021
8 min read
Most code I’ve written isn’t particularly special, let alone something to be proud of, but I’m perhaps unreasonably proud of this one.
Finding the correct parameter when writing code
Tracking the “next” parameter in a tooltip
The code is what powers which parameter in a tooltip gets bolded.
Basically, when you’re calling in F# function, there’s (now, after many years!) a tooltip that will pop up and tell you a few things:
- The full signature of the function you’re calling
- Any documentation for the function, sourced from XML docs
- Which parameter you’re “on”
You can see that last bit manifest in two ways, because there’s two different ways that the last bullet point should work:
- You’re actively calling the function and you want the parameter that’s “next up”
- You’re cycling back and forth between parameters already-applied to a function and editing them
In other words, the tooltip needs to display the parameter that comes next OR the parameter most relevant to where your cursor is at.
The code that finds the argument to show
Here’s the code that calculates the “correct” argument to display:
(*
Calculate the argument index for fun and profit! It's a doozy...
Firstly, we need to use the caret position unlike before.
If the caret position is exactly in range of an existing argument, pick its index.
The rest answers the question of, "what is the NEXT index to show?", because
when you're not cycling through parameters with the caret, you're typing,
and you want to know what the next argument should be.
A possibility is you've deleted a parameter and want to enter a new one that
corresponds to the argument you're "at". We need to find the correct next index.
This could also correspond to an existing argument application. Buuuuuut that's okay.
If you want the "used to be 3rd arg, but is now 2nd arg" to remain, when you cycle
past the "now 2nd arg", it will calculate the 3rd arg as the next argument.
If none of that applies, then we apply the magic of arithmetic
to find the next index if we're not at the max defined args for the application.
Otherwise, we're outa here!
*)
let! argumentIndex =
let caretTextLinePos = sourceText.Lines.GetLinePosition(caretPosition)
let caretPos = mkPos (Line.fromZ caretTextLinePos.Line) caretTextLinePos.Character
let possibleExactIndex =
curriedArgsInSource
|> Array.tryFindIndex(fun argRange -> rangeContainsPos argRange caretPos)
match possibleExactIndex with
| Some index -> Some index
| None ->
let possibleNextIndex =
curriedArgsInSource
|> Array.tryFindIndex(fun argRange -> Position.posGeq argRange.Start caretPos)
match possibleNextIndex with
| Some index -> Some index
| None ->
if numDefinedArgs - numArgsAlreadyAppliedViaPipeline > curriedArgsInSource.Length then
Some (numDefinedArgs - (numDefinedArgs - curriedArgsInSource.Length))
else
None
Nothing like the comment being just as long as the code itself.
Scenario examplanation
There’s several things at play here:
- The number of arguments that a function has defined in its signature
- The number of arguments the user has applied so far
- The current position of the editor caret in Visual Studio
Consder the following code, where |
represents an editor caret:
let add2 x y = x + y
add2 | // Tooltip should show 'x' as highlighted when editing code here
add2 1 | // Tooltip should show 'y' as highlighted when editing code here
add2 1 |2 // Tooltip should show 'y' as highlighted
add2 1| 2 // Tooltip should show 'x' as highlighted
If the caret is in the range of an existing argument in source that’s been applied to the function, it will report that as the argument to bold in the UI. That’s pretty straightforward - if you cycle through parameters with your caret, you’d expect to see those parameters highlighted.
Where it gets more interesting is the case when you’re editing source code. You may not have applied every parameter to a function yet! So Visual Studio needs to display the next parameter to apply.
In fact, this exact scenario is why the feature is called “Signature Help”. Using the signature of a function, it helps you by informing you of the next parameter you’re on - its name and its type - so that you can pass the right thing with less guessing. It’s a productivity feature!
If you’re simply working left-to-right and applying parameters as you go, it’s pretty easy to find the next parameter to display in the tooltip. It’s just whatever comes next!
But let’s say you wanted to apply a different parameter to something you’ve already applied … as you cycle through existing parameters with the caret, it will highlight them.
Re-adding a deleted parameter
But now let’s say you delete a parameter you’ve already applied:
let add2 x y = x + y
add2 | 2 // What should the tooltip display at '|'?
Here I’ve only applied 1 parameter, so from the F# compiler’s perspective, I’ve passed 2
as the parameter to x
. But that’s not what I’m doing! I deleted my x
parameter and I want to re-type it. Can Visual Studio help me with that?
Finding the correct parameter when writing code
But..how? I just said that as far as the F# compiler is concerned, 2
has been passed as the parameter to x
.
First, there’s no argument at the caret position, so this is all a game of trying to find the right “next” parameter.
Perhaps unintuitively, it’s this line of code that gives the answer this time around:
let possibleNextIndex =
curriedArgsInSource
|> Array.tryFindIndex(fun argRange -> Position.posGeq argRange.Start caretPos)
This says, “try to find the index of the argument already applied whose position in source is greater than or equal to the caret position”.
Since there’s one other argument at index 0, but it’s greater than the caret position, this index is going to actually be the “next” parameter to apply! And it works out.
Adding the next parameter at the end
So what about this code?
match possibleNextIndex with
| Some index -> Some index
| None ->
if numDefinedArgs - numArgsAlreadyAppliedViaPipeline > curriedArgsInSource.Length then
Some (numDefinedArgs - (numDefinedArgs - curriedArgsInSource.Length))
else
None
This is actually the most common case - when you’re applying the next parameter but it’s not in between pre-existing parameters:
Finding the correct parameter when editing existing code
Using this scenario:
let add2 x y = x + y
add2 1 | // caret should show 'y' here
If you plug it in, the code is:
if 2 - 0 > 1 then // true
Some (2 - (2 - 1)) // index 1 (2nd parameter)
else
None
And thus we get index 1
, the 2nd defined parameter to display in the pipeline.
But! What the fuck is up with that numArgsAlreadyAppliedViaPipeline
value? This is where it’s even more interesting.
Adding the next parameter in a pipelined function
Consider the following scenario:
[1..10]
|> List.map | // 'map' takes two parameters, but the LAST one is implicitly applied
The full signature of List.map
has two parameters, in order: a function to do the mapping, and the list to apply the mapping to. But the list is already applied via |>
! Showing it in a tooltip, especially as something that could be highlighted, would be pretty damn confusing. So we chop off that parameter for the purposes of the tooltip entirely!
However, chopping off that tooltip isn’t enough. We need to account for the fact that there may already have been a number of previously-defined arguments (1-3 to be exact, matching |>
, ||>
, and |||>
). That’s done in the code snippet!
match possibleNextIndex with
| Some index -> Some index
| None ->
if numDefinedArgs - numArgsAlreadyAppliedViaPipeline > curriedArgsInSource.Length then
Some (numDefinedArgs - (numDefinedArgs - curriedArgsInSource.Length))
else
None
For the following code:
[1..10]
|> List.map | // 'map' takes two parameters, but the LAST one is implicitly applied
Let’s plug it in:
if 2 - 1 > 0 then // true
Some (2 - (2 - 0)) // 0 - the first argument
else
None
And here we have it, we compute the 1st argument for List.map
, which is the mapping function - exactly what we wanted.
Finding the correct parameter for a pipeline
You can find all my other code crimes for Signature Help in this file .
What’s the point of this post?
Simple: I wrote this code and it makes me smile. It handles several scenarios to deliver the right developer experience, and is detailed enough to cover all the “little things” that make something pleasant. I’ve written a lot of features for the F# tools in VS, but this is easily my favorite one. And the code that powers it is my favorite F# code that I’ve written.
I guess another point of this post is that writing detailed editor tooling can be really, really fucking hard. There’s an enormous amount of time and care poured into even “basic” experiences in editor tooling. If you’re using an IDE with stuff like this, you’re benefiting from countless hours of hard work. But that work is worth it.
Cheers, and if you’re not using F# today, give it a whirl !
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK