golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.38k stars 17.7k forks source link

x/tools/gopls: Hover/DocumentHighlight for format strings #70050

Open findleyr opened 1 month ago

findleyr commented 1 month ago

This just occurred to me, when looking up a rarely used format verb for the nth time at pkg.go.dev/fmt: we should offer hover information for format strings. To start, this could simply show documentation for the active format verb under the cursor, but we could also extend it to explain more complicated formatting expressions.

We could extract the tricky bits of detecting and parsing format strings from the printf analyzer, though that begs the question of why we can't detect printf wrappers... @adonovan I wonder what you think about this idea, and more generally about the concept of exposing facts from the analysis framework to other gopls functionality.

Note that accurate detection of printf wrappers is not strictly necessary--that's more of a thought experiment. For hover, we could use a much weaker heuristic to guess whether a string is likely to be a format string.

adonovan commented 1 month ago

Format strings are only one small step removed from the language spec in the sense that every Go file uses them and every Go reader is expected to recognize them, or at least the dozen or so most common ones. Just as I would find it odd to serve a hover box over the go keyword that says it starts a new goroutine, it seems to me redundant to show a dialog over %q saying it quotes a string. Though it may benefit the beginner, I expect it would do so at the cost of the Go programmers who have passed that phase and find the extra popups a nuisance.

To your bigger implied questions:

findleyr commented 1 month ago

Though it may benefit the beginner, I expect it would do so at the cost of the Go programmers who have passed that phase and find the extra popups a nuisance.

I'm not following this argument. FWIW, this issue is premised on the fact that this would help me, and I'm not a beginner! We offer hover for builtins, even though they're part of the spec... Certainly one uses make more than %p. You may argue that 'make' depends on its arguments, but so does the behavior of %p! (And for the record, I never have to look up %q, even though its behavior is rather more complicated than others). In fact, I'd go so far as to say it would be nice to offer completion after typing %.

Unfortunately, you may be right that the hover could be a nuisance, but then aren't all hovers a nuisance? In my editor, I only trigger hover explicitly.

adonovan commented 1 month ago

Fair point. I remember I once asked you why we bothered with hover on built-ins and you responded that they're really useful.

I do think there would be real value in tying each %v formatting operation with its operand, as when they are numerous it can be hard to see the correspondence at a glance (which is why Python now has f-strings). Perhaps DocumentHighlight is the ideal LSP UI? I still have concerns about efficiency if lightweight queries such as Hover and Highlight were to invoke the analysis framework, even if only in particular cases with a restricted subset of analyzers. We hack up a little experiment.

xzbdmw commented 1 month ago

I'd go so far as to say it would be nice to offer completion after typing %

It would be so much better if

type Bar struct {}
var foo int
var bar Bar
fmt.Printf("%foo, %bar")

can expands to

type Bar struct {}
var foo int
var bar Bar
fmt.Printf("%d, %v", foo, bar)

via completion then a (range) code action, given that go has rejected f-string proposal, it is always a pain if arguments are too many. (why we need to specify the various %d %v etc.. if complier already knows the type)

xzbdmw commented 5 days ago

Perhaps DocumentHighlight is the ideal LSP UI? I still have concerns about efficiency if lightweight queries such as Hover and Highlight were to invoke the analysis framework, even if only in particular cases with a restricted subset of analyzers. We hack up a little experiment.

DocumentHighlight should solve the "counting" process quite well, I'm writing a lot long printf so I want to implement this feature, where should I start?

(after moving the undeclared analyzer to gopls/internal, the response speed improved quit a lot, I guess it is a good thing to slowly factor the printf analyzer out?)

adonovan commented 5 days ago

DocumentHighlight should solve the "counting" process quite well, I'm writing a lot long printf so I want to implement this feature, where should I start?

You'll need a function that, given a string literal and an offset, returns an index within the denoted string; and a function that, given a logical format string and an offset, returns the index of the % conversion (if any) that it falls within. The simplest way to do the second one would be to follow the approach in https://github.com/golang/go/issues/68279#issuecomment-2282907630, which isn't perfect, but is good enough for >99.9% of strings.

Once you have those, hooking into internal/golang/highlight.go for CallExpr shouldn't be hard. The only tricky bit is how to know which functions to apply it to: obviously fmt.Printf, but what about all the wrappers? We could use the analysis framework, but there are a lot of tricky problems there. Since this is only highlighting, it needn't be perfect: perhaps it's enough to observe that the format string contains a %, or approximately as many %s as arguments?

findleyr commented 5 days ago

Starting with DocumentHighlight makes sense to me.

after moving the undeclared analyzer to gopls/internal, the response speed improved quit a lot, I guess it is a good thing to slowly factor the printf analyzer out

FWIW, that's because there's a 1s debouncing before we run analysis.

xzbdmw commented 3 days ago

I think this can be simpler without parsing format string and args. The format syntax is %[flags][width][.precision][verb], what we need for DocumentHighlight is only the range of the placeholder, that is, a range starts with '%' and ends with a valid verb, and details sits between them can be ignored. So we can just find the '%', find the next verb, get the range & index, then we are nearly done.

or approximately as many %s as arguments

The main use case is to writing the arg list without counting which % I'm matching, so I think it is better to not assume any args, but try to highlight one if there is a match.

Update: let's include all information, since hover may want to show what is the meaning of a precision/width/flag.

xzbdmw commented 1 day ago

Should we also paint the format string with semantic highlight? Once we have a iter describing each element of a string in order, highlighting becomes free too, though vscode's textmate rule and treesitter-printf (I guess many people doesn't know it exists) can detect something similar. But it may benefit other editors.

gopherbot commented 1 day ago

Change https://go.dev/cl/632598 mentions this issue: gopls/internal/highlight: DocumentHighlight for format strings