Skip to content

Adds linked editing for JSX tags - #53284

Merged
Isabel Duan (iisaduan) merged 34 commits into
microsoft:mainfrom
iisaduan:mirror
Apr 7, 2023
Merged

Adds linked editing for JSX tags#53284
Isabel Duan (iisaduan) merged 34 commits into
microsoft:mainfrom
iisaduan:mirror

Conversation

@iisaduan

@iisaduanIsabel Duan (iisaduan) commented Mar 16, 2023

Copy link
Copy Markdown
Member

Adds linked editing of JSX tags (mirror cursor)

fixes#51832

@typescript-bot

Copy link
Copy Markdown
Contributor

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 Sheetal Nandi (@sheetalkamat), Matt Bierner (@mjbvz), Kat Marchán (@zkat), and Joaquin Jares (@joj) for you. Feel free to loop in other consumers/maintainers if necessary

@typescript-botTypeScript Bot (typescript-bot) added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Mar 16, 2023
@typescript-bot

Copy link
Copy Markdown
Contributor

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-botTypeScript Bot (typescript-bot) added the lib update PR modifies files in the `lib` folder label Mar 16, 2023
Comment threadtests/baselines/reference/api/typescript.d.ts Outdated
Comment threadtests/baselines/reference/api/tsserverlibrary.d.ts Outdated
@iisaduanIsabel Duan (iisaduan) changed the title Adds linked editing for JSX tags, as specified in #51832Adds linked editing for JSX tagsMar 17, 2023

Copy link
Copy Markdown
Member

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.

Comment threadsrc/harness/fourslashInterfaceImpl.ts
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadtests/cases/fourslash/jsxTagLinkedEdit7.ts Outdated
Comment threadtests/cases/fourslash/jsxTagLinkedEdit7.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
@iisaduan

Copy link
Copy Markdown
MemberAuthor

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

Copy link
Copy Markdown
Member

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.

Comment threadsrc/server/protocol.ts Outdated

export const enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
LinkedEditing = "LinkedEditing",

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 {

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
interfaceLinkedEditingRanges{
interfaceLinkedEditingRangesBody{

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

Comment threadtests/cases/fourslash/fourslash.ts Outdated
Comment threadsrc/harness/fourslashImpl.ts Outdated
Comment threadsrc/harness/fourslashInterfaceImpl.ts
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated
Comment threadsrc/services/services.ts Outdated

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

Copy link
Copy Markdown

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

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
Copy Markdown

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

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.

Comment threadsrc/server/session.ts Outdated
@iisaduan
Isabel Duan (iisaduan) marked this pull request as ready for review April 6, 2023 22:20
@typescript-bot

Copy link
Copy Markdown
Contributor

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

Comment threadtests/cases/fourslash/fourslash.ts Outdated
}

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

Copy link
Copy Markdown
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}[];

Comment threadtests/cases/fourslash/fourslash.ts Outdated

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

Copy link
Copy Markdown
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;

Comment threadsrc/services/services.ts Outdated
}
return false;
});
if (!element || !(isJsxOpeningElement(element) || isJsxClosingElement(element))) return undefined;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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?

@jakebaileyJake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
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. Nathan Shively-Sanders (@sandersn)'s new completion baselines.

Comment threadsrc/services/services.ts Outdated
// determines if the cursor is in an element tag
const element = findAncestor(token.parent,
n => {
if (!n.parent) return "quit";

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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

Comment threadsrc/services/services.ts Outdated
@typescript-botTypeScript 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
Copy Markdown
Contributor

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 freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Milestone BugPRs that fix a bug with a specific milestonelib updatePR 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

@iisaduan@typescript-bot@DanielRosenwasser@jakebailey@RyanCavanaugh@mjbvz