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

Adds linked editing for JSX tags #53284

Merged
merged 34 commits into from
Apr 7, 2023
Merged

Adds linked editing for JSX tags #53284

merged 34 commits into from
Apr 7, 2023

Conversation

iisaduan
Copy link
Member

@iisaduan iisaduan commented Mar 16, 2023

Adds linked editing of JSX tags (mirror cursor)

fixes #51832

@typescript-bot
Copy link
Collaborator

Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @mjbvz, @zkat, and @joj for you. Feel free to loop in other consumers/maintainers if necessary

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Mar 16, 2023
@typescript-bot
Copy link
Collaborator

It looks like you've sent a pull request to update our 'lib' files. These files aren't meant to be edited by hand, as they consist of last-known good states of the compiler and are generated from 'src/lib' or possibly our lib generator. Unless this is necessary, consider closing the pull request and sending a separate PR to update 'src/lib' or https://github.com/microsoft/TypeScript-DOM-lib-generator

@typescript-bot typescript-bot added the lib update PR modifies files in the `lib` folder label Mar 16, 2023
tests/baselines/reference/api/typescript.d.ts Outdated Show resolved Hide resolved
tests/baselines/reference/api/tsserverlibrary.d.ts Outdated Show resolved Hide resolved
@iisaduan iisaduan changed the title Adds linked editing for JSX tags, as specified in #51832 Adds linked editing for JSX tags Mar 17, 2023
Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

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

Looks like this is all coming together pretty nicely. I left a bit of feedback about

  • why things weren't working correctly with fragments,
  • some ideas for how fourslash testing can be made easier
  • some thoughts on the API (TextSpan vs. TextRange - maybe we revisit that?)
  • some style feedback

With regards to style, at some point we should consider getting an automatic formatter - but in the meantime, try to stay consistent with the rest of the codebase, and use the editor's built-in formatter as well.

src/harness/fourslashInterfaceImpl.ts Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
tests/cases/fourslash/jsxTagLinkedEdit7.ts Outdated Show resolved Hide resolved
tests/cases/fourslash/jsxTagLinkedEdit7.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
@iisaduan
Copy link
Member Author

I've renamed the feature to linkedEditing..., to match the LSP and allow for potential non-JSX tag usage in the future.

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

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

I think you could use a few more edge-case tests. Tests where you're at the beginning of the file, end of the file, and where the code is not syntactically valid (e.g. a closing tag missing its tag name, or a closing fragment tag that "accidentally" has a tag name.

@@ -23,6 +23,7 @@ import {

export const enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
LinkedEditing = "LinkedEditing",
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing LSP uses this name, but every other command type in the TS Server protocol is mostly camelCase.

interface LinkedEditingRequest extends FileLocationRequest {
readonly command: CommandTypes.LinkedEditing;
}
interface LinkedEditingRanges {
Copy link
Member

Choose a reason for hiding this comment

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

It's odd that the ranges are a subfield. Looking at existing precedent, there's FileReferencesResponseBody, so maybe:

Suggested change
interface LinkedEditingRanges {
interface LinkedEditingRangesBody {

Copy link
Member Author

Choose a reason for hiding this comment

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

I had written the LinkedEditingRanges and LinkedEditingRangeResponse this way, as this is how they were set up in the stubs and LSP

Copy link
Member

Choose a reason for hiding this comment

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

I would also prefer if it matched the style we had in tsserver and was named Body; LSP may define it one way but I don't think we should be inconsistent to match them.

tests/cases/fourslash/fourslash.ts Outdated Show resolved Hide resolved
src/harness/fourslashImpl.ts Outdated Show resolved Hide resolved
src/harness/fourslashInterfaceImpl.ts Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved
src/services/services.ts Outdated Show resolved Hide resolved

return {
ranges: [{ start: openTagStart, length: openTagEnd - openTagStart }, { start: closeTagStart, length: closeTagEnd - closeTagStart }],
wordPattern: openingTagText
Copy link
Contributor

Choose a reason for hiding this comment

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

This should actually be regex for valid tag names. Something like [a-z\._$][a-z0-9\._$]+ perhaps. You can also try omitting it if VS Code's word pattern is good

With the current implementation, VS Code stops linked editing as soon you type anything that is not the tag name

Copy link
Member

Choose a reason for hiding this comment

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

What happens if the returned ranges don't satisfy the regex? No linked cursors I assume?

I think the problem with that regex is that it expects at least two characters. Maybe you wanted:

[a-z\._$][a-z0-9\._$]*

?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but also try omitting it and seeing if VS Code does the right thing already. You should only need to return wordRange if the default behavior isn't correct

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, in that case let's leave off wordPattern for now.

@iisaduan iisaduan marked this pull request as ready for review April 6, 2023 22:20
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@@ -759,6 +760,11 @@ declare namespace FourSlashInterface {
generateReturnInDocTemplate?: boolean;
}

type LinkedEditingInfo = {
readonly ranges : { start: number, length: number }[];
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
readonly ranges : { start: number, length: number }[];
readonly ranges: { start: number, length: number }[];

@@ -759,6 +760,11 @@ declare namespace FourSlashInterface {
generateReturnInDocTemplate?: boolean;
}

type LinkedEditingInfo = {
readonly ranges : { start: number, length: number }[];
wordPattern? : string;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
wordPattern? : string;
wordPattern?: string;

}
return false;
});
if (!element || !(isJsxOpeningElement(element) || isJsxClosingElement(element))) return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

Is (isJsxOpeningElement(element) || isJsxClosingElement(element)) possibly false given what the findAncestor call does? Fine to leave in just in case, but I'm curious if this/how this happens

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems to now be mostly for the typeguard feature, as it seems other code I've added since covers these cases. Should I change it to an as JsxOpeningElement | JsxClosingElement | undefined assertion to the result of findAncestor?

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

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

Things seem good, but, I think I would like to see the protocol naming be consistent, as once we add it, we're not going to change it.

interface LinkedEditingRequest extends FileLocationRequest {
readonly command: CommandTypes.LinkedEditing;
}
interface LinkedEditingRanges {
Copy link
Member

Choose a reason for hiding this comment

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

I would also prefer if it matched the style we had in tsserver and was named Body; LSP may define it one way but I don't think we should be inconsistent to match them.

verify.linkedEditing( {
"0": linkedCursors,
"1": linkedCursors,
"2": undefined,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this is a lot of work, but maybe it's better to have a baseline for this instead, rather than all of these undefineds? I know that a lot of our current tests have this, but, I think we're trying to make them friendlier, e.g. @sandersn's new completion baselines.

// determines if the cursor is in an element tag
const element = findAncestor(token.parent,
n => {
if (!n.parent) return "quit";
Copy link
Member

Choose a reason for hiding this comment

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

Is this required? I think the only case where you won't have a parent is on SourceFile, in which case iteration is going to stop.

Copy link
Member Author

Choose a reason for hiding this comment

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

It caused an issue when you checked the kind of .parent, and .parent is undefined, but turns out, I was able to rewrite findAncestor so I didn't need to check .parent anymore

src/services/services.ts Outdated Show resolved Hide resolved
@typescript-bot typescript-bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Apr 7, 2023
@typescript-bot
Copy link
Collaborator

The TypeScript team hasn't accepted the linked issue #51832. If you can get it accepted, this PR will have a better chance of being reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone lib update PR modifies files in the `lib` folder
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mirror Cursor for JSX
6 participants