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

Universal dot completion #1054

Merged
merged 34 commits into from
Jan 2, 2025
Merged

Universal dot completion #1054

merged 34 commits into from
Jan 2, 2025

Conversation

zth
Copy link
Collaborator

@zth zth commented Nov 5, 2024

Broadens the scope for dot completions to (in addition to record fields) cover:

  • Object fields if it's an object
  • Pipeable functions from the parent module
  • Pipeable functions from any extra module configured via @editor.completeFrom on the type being completed

The editor will automatically handle inserting what you complete for properly into the code, so even if you trigger a completion with a dot, that dot will be removed if you select a completion that's not an actual record field.

Example:

module SomeModule = {
  @editor.completeFrom(SomeModuleUtils)
  type t = {name: string}

  @get external getName: t => string = "name"
}

module SomeModuleUtils = {
  let doStuff = (t: SomeModule.t) => t
}

let n = {SomeModule.name: "hello"}

// n.
//   ^com

This should complete for both name (record field), getName (function from parent module that takes this record as the first argument) and doStuff (pipable function from module defined via @editor.completeFrom).

Experimental @editor.completeFrom annotation

This PR also introduces an experimental @editor.completeFrom annotation, that lets you provide more modules the tooling can draw additional completions from. This is useful because there are situations where you need to draw completions from another module than what the type belongs to, or when you can't put the type inside of its own module (and get completions via that). Examples include recursive type chains, where the type definition might need to live outside of the module, and similar.

Annotating with @editor.completeFrom will make the editor tooling complete for pipes from the provided module too. Example:

// We point out `SomeOtherModule` as the module this type is the main type for
@editor.completeFrom(SomeOtherModule)
type typeOutsideModule = {nname: string}

module SomeOtherModule = {
  type t = typeOutsideModule
  @get external getNName: t => string = "nname"
  @get external getNName2: typeOutsideModule => string = "nname"
}

let nn = {nname: "hello"}

// nn.
//    ^com

This will complete for:

  • nname (record field)
  • getNName (pipe from module)
  • getNName2 (pipe from module)

You can also add several modules either via several instances of the attribute, or by passing an array: @editor.completeFrom([ModuleA, moduleB]) type xx.

The annotation has no other effect than providing a hint for the editor tooling.

Comment on lines 16 to 144
"label": "->SomeModule.getName",
"kind": 12,
"tags": [],
"detail": "t => string",
"documentation": null,
"sortText": "getName",
"insertText": "->SomeModule.getName"
}, {
"label": "name",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nname: string\n```\n\n```rescript\ntype t = {name: string}\n```"}
}]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the relevant test output.

@zth zth changed the title PoC complete pipeable functions from dot completion on record Complete pipeable functions from dot completion on record Nov 6, 2024
@zth zth marked this pull request as ready for review November 6, 2024 21:27
Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

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

Any chances this affects library design? Core?

Path n
Path SomeModule.
[{
"label": "->SomeModule.getName",
Copy link
Collaborator

Choose a reason for hiding this comment

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

curious about the function coming before the field
also, any thoughts about making . a valid source language construct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could you expand a bit?

Copy link
Collaborator

@cristianoc cristianoc Nov 7, 2024

Choose a reason for hiding this comment

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

1 If I write x. I see x->getName() before x.field, which can be surprising to people working with records. Just wondering.

2 Could x.getName() be actually part of the language, or will it always need to be expressed as x->getName? Autocompletion is right now suggestive of something that does not exist in the language. Not necessarily a bad thing, just thinking aloud about possible pros and cons here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. Ahh right! That's a good point. It's currently sorted on the field name and function name. We could sort it whatever way we think works best. I'd say either this (sort it together with the regular fields) or put it at the bottom. I'd favor the current way, I think.
  2. Oh, that's.... a very interesting idea! That would solve quite a few of our issues. Let's explore that!

Copy link
Member

Choose a reason for hiding this comment

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

It seems that is about what called "uniform function call syntax"

And there was a similar discussion in ReasonML reasonml/reason#1638 early day

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for the context. Will read through.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wonder if we could figure out a good way to do that, pointing out the module to use with the call (someRecord.SomeModule.someFn()), but with a small enough twist that it's still more convenient than pipe, but not ambigious wrt the current behavior of SomeModule there being a hint.

Anyway, just completing the actual pipe, but via dot, is a good start for DX imo.

@zth
Copy link
Collaborator Author

zth commented Nov 7, 2024

Any chances this affects library design? Core?

I don't think so. It should mostly just simplify things, and I also think with this we could get rid of some the special casing for pipe completions we have, and just add the annotation to types shipped with the compiler.

@zth
Copy link
Collaborator Author

zth commented Nov 7, 2024

Ok, so this PR explicitly handles (resolvable) record types only, because this was originally intended to cater to a potential use case in https://github.com/rescript-lang/experimental-rescript-webapi. But, this could easily be useful in more general cases. Examples:

  • We could apply the same logic for pipe completions in general. Ie if you have a type where @mainTypeForModule is defined, then complete also from that module when doing pipe completion.
  • Given that whatever. is such an established concept in JS/TS, we really don't need to restrict this to records only. We could just generalize this pattern to work on any dot-completion, as long as the type that dot completion resolves to is a type t of a module, or annotated with a @mainTypeForModule

The latter part opens up some pretty good optimizations for people used to JS/TS:

"some string".
//            ^com

JS/TS users would expect the above to complete for length, since string.length is how you get the length of a string in JS. In ReScript, you'd do "some string"->String.length. Currently that's difficult to discover though unless you know about it, because you need to explicitly do pipe completion.

Expanding dot completion to also complete for relevant pipe functions here would make that much more discoverable. I would also think it would mean there's a good chance people will just learn the convention of "main type, pipe functions from module of that main type" automatically because they'll see what their expected dot completion is resolved to. This would be a massive win because that means that the tooling could help teach one of the larger differences between how things are done in ReScript vs JS.

// ^com
// ^dv-

@mainTypeForModule(SomeOtherModule)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this also work when SomeOtherModule is another file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's effectively just a path to a module, so it'll take that exact module path and try to complete it from whatever point you're completing. So it's simulating doing whateverToComplete-><pathFromMainTypeForModuleHere>.<com>.

type typeOutsideModule = {nname: string}

module SomeOtherModule = {
type t = typeOutsideModule
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this alias as t a hard requirement to make this work?
Being newish to ReScript type t is a little strange and it feels a bit like an artificial limitation right now.

@zth zth changed the title Complete pipeable functions from dot completion on record Universal dot completion Nov 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants