Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/tools/gopls: add location-aware snippets for append and other utility methods #39507

Open
A-UNDERSCORE-D opened this issue Jun 10, 2020 · 21 comments

Comments

@A-UNDERSCORE-D
Copy link

@A-UNDERSCORE-D A-UNDERSCORE-D commented Jun 10, 2020

Is your feature request related to a problem? Please describe.
After using GoLand for some time I'm missing a small feature or two. Specifically I'm missing type aware snippets for append, sprintf, and a few other utility methods (like things from slicetricks).

Describe the solution you'd like
I'd like a snippet under the name .aapend (auto-append) or similar to construct an append-assign statement for me.

Either from the start by starting with append and getting toAppend = append(toAppend, <>)

Or from the middle of a line using something like .aapend which would add the correct = append(name, <>) to the end of the line.

The sprintf and slicetricks I mentioned above would work similarly, sprintf simply going from thing.sprint to fmt.Sprintf("<>", thing), and delete applying a (preferably pointer aware delete implementation from the slicetricks wiki page

Describe alternatives you've considered
I've written my own snippets for this, but they get suggested in comments and other places where they are not useful, cluttering any autocomplete I may have had. And these rely on somewhat brittle regexps, which do not always get it right.

	"assign append": {
		"description": "appends to a slice and assigns it to itself",
		"prefix": "aappend",
		"body": "${1:toAppend} = append(${1:toappend}, $2)"
	},
	"assign append inplace": {
		"description": "appends to a slice and assigns it to itself",
		"prefix": ".aappend",
		"body": " = append(${TM_CURRENT_LINE/\\s*(\\S+)\\..*/$1/}, $0)"
	},
	"slice delete": {
		"description": "cuts a value from a slice",
		"prefix": ".delete",
		"body": " = append(${TM_CURRENT_LINE/\\s*(\\S+)\\..*/$1/}[:${1:pos}], ${TM_CURRENT_LINE/\\s*(\\S+)\\..*/$1/}[${1:pos}+1:]...)"
	}

The other option here is to type things manually, though that has the annoyance that for some things, you're going to end up checking a wiki page EVERY time (slicetricks things, delete et al.) or are just repetitive--typing thing = append(thing, newStuff) gets old. Not to mention that if its type aware it also suggest thing = append(thing, newStuff...) where appropriate.

This may be more applicable to gopls itself, rather than here, but I thought I'd ask about it here first.

@hyangah
Copy link
Contributor

@hyangah hyangah commented Jun 10, 2020

@A-UNDERSCORE-D Yeah, Goland's context-aware, custom postfix completion support is nice.

As you mentioned, we want to delegate most completion logic to the language server and this is the tracking issue #39354.

The completion snippets around append, or common standard functions, slice tricks are good completion candidates to be offered by default. It would be nice if customization and template support can be offered in the feature, too. @stamblerre, thought?

@stamblerre
Copy link
Contributor

@stamblerre stamblerre commented Jun 10, 2020

Thanks for the suggestion, @A-UNDERSCORE-D. In general, this does sound like a gopls feature request so I'll transfer it to the golang/go repo.

We actually should have some similar functionality around append already: https://go-review.googlesource.com/c/tools/+/221777. Maybe there are ways we can improve it, because it doesn't trigger when you write append, only when you are completing the slice variable. #39354 is definitely also relevant here.

@stamblerre stamblerre transferred this issue from golang/vscode-go Jun 10, 2020
@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Jun 10, 2020

Thanks for the transfer! And yeah Ive noticed append popping up like that on occasion, it works quite well. I guess my real want here is a more direct way to tell gopls "please give me this specific autocompletion" as shown in #39354 already. Not sure if I should keep this open or defer to the #39354? I think mine provides a bit more abstract around expected behavior for postfix style completions.

That said #39354 may be an opening for something more abstract where there can be a predefined set of postfixes that can be expanded on via config if a user desires.

@stamblerre stamblerre changed the title Add location aware snippets for append and other utility methods x/tools/gopls: add location-aware snippets for append and other utility methods Jun 11, 2020
@gopherbot gopherbot added this to the Unreleased milestone Jun 11, 2020
@stamblerre
Copy link
Contributor

@stamblerre stamblerre commented Jun 11, 2020

Yeah, I agree this can be a more general issue. Let's start with seeing how we can improve append and then think of more cases from there.

@golang golang deleted a comment from gopherbot Jun 11, 2020
@hyangah
Copy link
Contributor

@hyangah hyangah commented Jul 1, 2020

FYI the CL (https://go-review.googlesource.com/c/tools/+/221777) that implements autocompletion for append is merged and available in gopls 0.4.2.

@A-UNDERSCORE-D when is the second command 'assign append inplace' rule useful? (We are trying to understand use cases and how to improve the autocompletion logic)

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Jul 1, 2020

@hyangah Its mostly just an ease of use thing I picked up from my time using goland. Its a part of the whole postfix completion request. For us with gopls the main thing I can think of its its an unambiguous way to ask gopls to provide the completion we want. Meaning that instead of typing the partial name of a slice and using arrow keys to grab the append (I have not tried 4.2 yet, so the arrow keys comment is purely speculative) meaning that one's right hand needs to move to the right for arrow keys, one can use the progression partial name -> .aa (suggested append at this point) to get the same result. Or that is the intent, anyway

@segevfiner
Copy link
Contributor

@segevfiner segevfiner commented Jul 20, 2020

I'm not getting completions for the case foo = app<> // offer "append(foo, <>)", but only for fo<> // offer "foo = append(foo, <>)" with gopls 0.4.3 and VS Code 1.47.2.

@stamblerre stamblerre removed this from the Unreleased milestone Jul 23, 2020
@muirdm
Copy link

@muirdm muirdm commented Jul 30, 2020

it doesn't trigger when you write append, only when you are completing the slice variable

I'm not getting completions for the case foo = app<> // offer "append(foo, <>)"

Strange, it works great in Emacs 😛. I have no idea why VSCode isn't showing the candidate. In fact, it doesn't show any candidates in this case:

var foo []int
foo = app<>

Candidates from gopls:

[Trace - 20:54:09.159 PM] Received response 'textDocument/completion - (7)' in 100ms.
Result: {"isIncomplete":true,"items":[{"label":"append(foo, )","kind":3,"preselect":true,"sortText":"00000","insertTextFormat":2,"textEdit":{"range":{"start":{"line":4,"character":7},"end":{"line":4,"character":10}},"newText":"append(foo, ${1:})"}},{"label":"append","kind":3,"detail":"func(slice []Type, elems ...Type) []Type","sortText":"00001","filterText":"append","insertTextFormat":2,"textEdit":{"range":{"start":{"line":4,"character":7},"end":{"line":4,"character":10}},"newText":"append(${1:slice []Type}, ${2:elems ...Type})"}}]}

Should I open a separate (VSCode?) issue @stamblerre?

@stamblerre
Copy link
Contributor

@stamblerre stamblerre commented Jul 30, 2020

Oh, I actually didn't realize that was supposed to work - thanks for clarifying. I just opened golang/vscode-go#441 - we should probably investigate it in VS Code Go first, but you're right in thinking it may be a VS Code issue.

@hyangah
Copy link
Contributor

@hyangah hyangah commented Jul 31, 2020

@muirdm What's the vscode version? The candidate appears in my vscode. Maybe I am missing something.

@stamblerre stamblerre added this to the gopls/unplanned milestone Oct 21, 2020
@muirdm
Copy link

@muirdm muirdm commented Nov 23, 2020

I'm playing around with postfix completions if anyone wants to try it and give feedback: https://go-review.googlesource.com/c/tools/+/272586

You can see the list of postfix thingies available here: https://go-review.googlesource.com/c/tools/+/272586/1/internal/lsp/source/completion/postfix.go#82

The list is not necessarily complete, and the things in the list are not necessarily useful enough to be in the list. I was just trying a variety of things to see how they would work.

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Nov 24, 2020

Playing around with it a bit now. I really like this. the ! indicates its a little snippet, which is very helpful. And what you have is quite nice, I'd probably use them all over time.

Only complaint is that there arent docs sent with it. Not sure if that can be changed or not.

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Nov 24, 2020

is there a way I can add suggestions or otherwise to your CL @muirdm? Ive been considering implementing some extra ones

@muirdm
Copy link

@muirdm muirdm commented Nov 24, 2020

You can add things to the list in https://go-review.googlesource.com/c/tools/+/272586/1/internal/lsp/source/completion/postfix.go#82 and then rebuild gopls.

If you mean push changes to my branch, then I don't know. I assume there is some way to collaborate like that. My branch is rough and likely to change a lot, so I wouldn't put in too much time.

Only complaint is that there arent docs sent with it.

Do you mean more detail showing what the snippet will do, or actual textual documentation explaining it what it will do?

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Nov 25, 2020

yeah, a "This is what you'll get if you use this" kind of idea. IIRC vscode can show a little box next to the completion option? Im not 100% sure but I think that its a thing

@muirdm
Copy link

@muirdm muirdm commented Nov 25, 2020

This is what you'll get if you use this

Yes, gopls can fill in the completion item "detail" for that I think.

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Nov 30, 2020

This is what you'll get if you use this

Yes, gopls can fill in the completion item "detail" for that I think.

Poked around a bit in that, was pretty easy. Still getting used to how gopls works internally. I think this is reasonable though. Also your code appeared to rebase fine to the latest release, but not to master -- I definitely dont know enough to fix the conflict though -- Not sure how to resolve the two different edit methods.

diff --git a/internal/lsp/source/completion/postfix.go b/internal/lsp/source/completion/postfix.go
index 1f95cff2..3eb62e67 100644
--- a/internal/lsp/source/completion/postfix.go
+++ b/internal/lsp/source/completion/postfix.go
@@ -71,6 +71,7 @@ func and(filters ...postfixFilter) postfixFilter {
 
 type postfixRule struct {
 	name    string
+	details string
 	filter  postfixFilter
 	label   string
 	imports []string
@@ -84,52 +85,60 @@ var postfixRules = []postfixRule{{
 	filter:  isSLice,
 	label:   "sort",
 	imports: []string{"sort"},
+	details: "Insert a sort.Slice implementation",
 	body: `{{.Import "sort"}}.Slice({{.Obj}}, func(i, j int) bool {
 	{{.Cursor}}
 })`,
 }, {
-	name:   "slice_last",
-	filter: isSLice,
-	label:  "last",
-	body:   "{{.Obj}}[len({{.Obj}})-1]",
+	name:    "slice_last",
+	filter:  isSLice,
+	label:   "last",
+	body:    "{{.Obj}}[len({{.Obj}})-1]",
+	details: "Access the last item in this slice",
 }, {
-	name:   "slice_reverse",
-	filter: isSLice,
-	label:  "reverse",
+	name:    "slice_reverse",
+	filter:  isSLice,
+	label:   "reverse",
+	details: "Reverse this slice (in-place)",
 	body: `for i, j := 0, len({{.Obj}})-1; i < j; i, j = i+1, j-1 {
 	{{.Obj}}[i], {{.Obj}}[j] = {{.Obj}}[j], {{.Obj}}[i]
 }`,
 }, {
-	name:   "slice_range",
-	filter: isSLice,
-	label:  "range",
+	name:    "slice_range",
+	filter:  isSLice,
+	label:   "range",
+	details: "Iterate over this slice",
 	body: `for i, {{index .Vars 0}} := range {{.Obj}} {
 	{{.Cursor}}
 }`,
 }, {
-	name:   "slice_append",
-	filter: and(isSLice, isAssignable),
-	label:  "append",
-	body:   `{{.Obj}} = append({{.Obj}}, {{.Cursor}})`,
+	name:    "slice_append",
+	filter:  and(isSLice, isAssignable),
+	label:   "append",
+	details: "Append to this slice",
+	body:    `{{.Obj}} = append({{.Obj}}, {{.Cursor}})`,
 }, {
-	name:   "map_range",
-	filter: isMap,
-	label:  "range",
+	name:    "map_range",
+	filter:  isMap,
+	label:   "range",
+	details: "Iterate over this map",
 	body: `{{$v := .Vars}}for {{index $v 0}}, {{index $v 1}} := range {{.Obj}} {
 	{{.Cursor}}
 }`,
 }, {
-	name:   "map_clear",
-	filter: isMap,
-	label:  "clear",
+	name:    "map_clear",
+	filter:  isMap,
+	label:   "clear",
+	details: "Clear this map",
 	body: `{{$k := (index .Vars 0)}}for {{$k}} := range {{.Obj}} {
 	delete({{.Obj}}, {{$k}})
 }
 	`,
 }, {
-	name:   "map_keys",
-	filter: isMap,
-	label:  "keys",
+	name:    "map_keys",
+	filter:  isMap,
+	label:   "keys",
+	details: "Create a slice of all the keys in this map",
 	body: `keys := make([]{{index .Types 0}}, 0, len({{.Obj}}))
 {{$k := (index .Vars 0)}}for {{$k}} := range {{.Obj}} {
 	keys = append(keys, {{$k}})
@@ -145,24 +154,28 @@ var postfixRules = []postfixRule{{
 	name:    "print_scalar",
 	filter:  isScalar,
 	label:   "print",
+	details: "Print the %v of this variable",
 	imports: []string{"fmt"},
 	body:    `{{.Import "fmt"}}.Printf("%v\n", {{.Obj}})`,
 }, {
 	name:    "print_multi",
 	filter:  isMulti,
 	label:   "print",
+	details: "Print all of these",
 	imports: []string{"fmt"},
 	body:    `{{.Import "fmt"}}.Println({{.Obj}})`,
 }, {
 	name:    "warn_scalar",
 	filter:  isScalar,
 	label:   "warn",
+	details: "Print this to stderr",
 	imports: []string{"fmt", "os"},
 	body:    `{{.Import "fmt"}}.Fprintf({{.Import "os"}}.Stderr, "%v\n", {{.Obj}})`,
 }, {
 	name:    "warn_multi",
 	filter:  isMulti,
 	label:   "warn",
+	details: "Print all these to stderr",
 	imports: []string{"fmt", "os"},
 	body:    `{{.Import "fmt"}}.Fprintln({{.Import "os"}}.Stderr, {{.Obj}})`,
 }}
@@ -341,6 +354,8 @@ Rules:
 			Kind:                protocol.SnippetCompletion,
 			snippet:             snip,
 			AdditionalTextEdits: edits,
+			Documentation:       snip.String(),
+			Detail:              rule.details,
 		})
 	}
 }
@muirdm
Copy link

@muirdm muirdm commented Dec 20, 2020

I've updated the cl to push more logic into the completion templates. This way gopls can theoretically support dynamic user supplied postfix completion templates specific to the user's code base. The template framework probably needs a few more facilities to make this actually useful (for example a way to test for a specific type, e.g. {{if eq .Type "myproject/somepkg.SomeType"}}).

I also added basic "details" description for each completion. I don't think including the snippet text as the documentation is a good idea since that would include inscrutable snippet meta information, depending on the snippet.

TODOs:

  • duplicate object names in "vars"
  • inconsistent indent insertion in different editors
  • type aliases probably don't do the right thing
  • tests

@stamblerre can you take a preliminary look and either bless or condemn the general approach?

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 10, 2021

Change https://golang.org/cl/272586 mentions this issue: internal/lsp/source/completion: add postfix snippet completions

@muirdm
Copy link

@muirdm muirdm commented Jan 10, 2021

I've pushed a more polished CL to drive things forward. Most of the issues I know about are resolved. I'm curious to hear what others think of this feature. Personally I doubt I will remember to use them, and most of the snippets don't really compel me.

@A-UNDERSCORE-D
Copy link
Author

@A-UNDERSCORE-D A-UNDERSCORE-D commented Jan 10, 2021

Built, I'll play with it sometime when I next write go.

And yeah its very much not for everyone, its just how my thought process works, I'm typing as I think, so "use x and do ..." is where I'm going

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
6 participants