-
Notifications
You must be signed in to change notification settings - Fork 33.3k
Check for onEnterRules that decrease indentation when determining new line indent #136593
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
base: main
Are you sure you want to change the base?
Conversation
I rebased this to fix a merge conflict. |
@hediet, this fixes the following PHP code. If you hit Enter on the last blank line, it will add a space in front of the new line. /**
* PHPDoc
*/ Similarly, if you have the following code and hit Enter on the blank line, it will add a tab in front of the new line. If #136577 is merged, this PR will solve this issue too. if (1)
1; This issue is also a problem with other languages that allow single line conditionals. I think #136577 is much more impactful than this PR. This PR is also riskier than that one. I think it's the correct fix, but don't want to rush this one out if it needs more review time. Thanks! |
@aiday-mar I've noticed that you self-assigned indentation-related opened issues. Could you also please take a look at this PR if you get a chance? Currently dedention rules are non-sticky and we notice lots of cases like:
Where enter correctly dedents the first time:
But on subsequent times enter suddenly reindents:
|
Hi @lionel- thank you for pinging on this issue. For the example you gave I believe you could specify a new regex pattern in the |
Hi @aiday-mar, thanks for looking at this. I'm working on an improved language configuration for the R language. One important use case for R is pipelines of the form: # ggplot2 pipeline
mtcars |>
ggplot(aes(
disp,
drat
)) +
geom_point(
aes(colour = cyl)
)
# dplyr pipeline
starwars |>
group_by(species) |>
summarise(
n = n(),
mass = mean(mass, na.rm = TRUE)
) |>
filter(
n > 1,
mass > 50
) We have not been able to find a way to correctly indent this kind of pipelines with components spread over multiple lines, which is partly why I opened #208985. However it should at least be possible to indent one-liners pipelines such as: starwars |>
group_by(species) |>
summarise( n = n(), mass = mean(mass, na.rm = TRUE)) |>
filter(n > 1, mass > 50) Regarding your idea of using increase-indent-rule.movTo avoid this, we are using "onEnterRules": [
// A line ending with an operator, preceded by a blank line.
{
"previousLineText": "^\\s*(?:#|$)",
"beforeText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"action": { "indent": "indent" }
},
// A line ending with an operator, preceded by a line not ending with an operator.
{
"previousLineText": "^(?!.*[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$))[^#]+.*$",
"beforeText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"action": { "indent": "indent" }
},
// A line not ending with an operator, preceded by a line ending with an operator.
{
"previousLineText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"beforeText": "^(?!.*[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$))[^#]+.*$",
"action": { "indent": "outdent" }
},
] With these rules we get the correct indenting behaviour: onenter-rule.movUnfortunately the "outdent" action is not sticky, causing the following indentation bug on ulterior lines: outdent-stickiness-bug.movWe have verified that the PR fixes the non-stickiness of OnEnter outdent rules for our use case. |
Another case of this occurs in the javascript extension. Type Enter with this snippet: while (true)
foo;<CURSOR> You end up here: while (true)
foo;
<CURSOR> Type enter again and you get: while (true)
foo;
<CURSOR> |
Hi @lionel- thanks for pinging me on this PR. Before merging this PR, I need to look more closely at the code in the feature area. As I have only recently been assigned this feature area, I will need some time to analyze the logic. I will look into this in due time. |
While testing this branch we noticed a problem in the way the const afterEnterText = model.getLineContent(lineNumber); With the rules described in #136593 (comment) and this branch, open a file and paste: foo %>%
bar() Make sure the |
@aiday-mar, thanks for looking at this. Let me know if you want me to make any changes or rebase. This PR is so old. I didn't think it would ever get looked at. @lionel-, I created this PR about 2.5 years ago, so I'm not 100% sure, but I think that the issue with hitting Enter a second time is a limitation of the way the indentation logic works, but I figured at least it solves the use case of hitting Enter once. |
The problem is that no new line is entered after the first time. That's because the line I quoted tries to retrieve a line that doesn't exist in the text model and throws an error that is silently swallowed by the |
@lionel-, I was referring to #136593 (comment). I honestly didn't read through your comments about the R language because I thought they were directly towards @aiday-mar and I have never worked with R before. If this PR does break something that used to work with R, but doesn't with this PR, I definitely should look into solving that. However, at the moment, I don't plan on spending more time on this. I spent a lot of time a few years ago researching issues and submitting PRs, but then I couldn't really get anyone from Microsoft to look at many. So I've given up on spending time on submitting VS Code PRs. I'll pick up work on this again if someone from Microsoft indicates that they plan on reviewing and potentially merging it. |
Is this still relevant? |
I think so. I just wasn't able to get enough traction from Microsoft to get it merged. I can pick it up again if someone from Microsoft indicates that it's worthwhile. |
So looking at the comments here it seems to me that the main issue is that when pressing |
That sounds reasonable to me. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ssigwart thank you so much for the PR and apologies this took a long time to review. I have looked at the PR and thought some more about it. I think we could do the following:
- First of all I think using the onEnter rules to compute inherited indent should not be default behavior right now. This is because we don't know what kind of unexpected consequences it could have. As a result I suggest we add this logic under a setting which will be false by default. Users will be able to opt into this behavior and file bugs, which we can use to improve this logic further. If we notice that generally this logic improves indentation, we can consider making the default true or entirely removing the setting.
- I am wondering why the PR applies the onEnter rules only in the case
honorInitialIndent
is true. I am thinking if we apply the onEnter rules we should apply them in all the cases so we have consistent behavior? - Lastly I noticed that the PR only applies the onEnter result when it does an outdentation and it removes text. I think ideally if we go forwards with this, we should apply the onEnter rules in all cases, whether it does an indentation or an outdentation. This way the indentation will be predictable.
Thank you for taking the time to make this PR.
const richEditSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()); | ||
if (richEditSupport) { | ||
const previousLineText = precedingUnIgnoredLine < 1 ? '' : model.getLineContent(precedingUnIgnoredLine - 1); | ||
const afterEnterText = model.getLineContent(lineNumber); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like here we are calculating the onEnter rules only in the case when honorIntentionalIndent
is true. Is there a reason why we do not also calculate the onEnter rules in the other return cases too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't actually remember. My guess is that I was debugging and found that this was the place that cause the issue, so only updated that.
In the latest round of changes, I started refactoring this so I could call in in other places, but I see that the other places that call strings.getLeadingWhitespace
seem to be places that either explicitly say to change the indent or are line 1, which doesn't need this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing in @aiday-mar. I got this up to date with main
and made updates.
const richEditSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()); | ||
if (richEditSupport) { | ||
const previousLineText = precedingUnIgnoredLine < 1 ? '' : model.getLineContent(precedingUnIgnoredLine - 1); | ||
const afterEnterText = model.getLineContent(lineNumber); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't actually remember. My guess is that I was debugging and found that this was the place that cause the issue, so only updated that.
In the latest round of changes, I started refactoring this so I could call in in other places, but I see that the other places that call strings.getLeadingWhitespace
seem to be places that either explicitly say to change the indent or are line 1, which doesn't need this code.
try { | ||
afterEnterText = model.getLineContent(currentLineNumber); | ||
} catch (e) { | ||
// Probably after the end of file | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an issue if the last line of the file is if (1)
and you hit enter. I feel like there's probably a better way to do this, but couldn't find an easy was to see if it was passed the end of the file.
Great thanks I will have a look again |
HI @ssigwart thank you for making these changes. I have looked some more at the PR and at the issue the PR is aiming to fix. I noticed the original issue you mentioned was that after:
The editor adds a new line on the next line, and that after:
The editor should outdent. Both of these cases have already been fixed with some language configuration file changes for JavaScript and TypeScript, here and here. After some more thought I realized we could make the same change in the PHP language configuration file to fix the incorrect indentation without having to make more complicated changes. Should we do this instead? Maybe we still want to make this more complicated change however but I am debating if this is really necessary and wanted to discuss this here. I think @lionel- gave some reasons for why this would be necessary. @lionel- could you perhaps share your extension code so I understand better the need for considering If we go forward with considering
I am currently not sure if we should proceed with this PR however. |
@aiday-mar, the second link (typescript config) is a change I made in #136577 that I referenced at the top of this PR. There's are slight differences. The other PR that was merged fixed hitting enter if the cursor is where the if (1)
code();| Previously, the next line would start indented. That is fixed already. This PR is to fix hitting enter here: if (1)
code();
| There will be an extra indent like this: if (1)
code();
| A similar thing happens with PHPDoc. My thought in this PR was that |
Hi, may I ask what settings you are using? I see the following on the latest insiders with very minimal settings: Screen.Recording.2025-06-11.at.12.19.34.mov |
I tested JavaScript and Typescript and I don't see the problem. I do see the problem with PHP and C though. My settings are pretty minimal in Insiders: {
"workbench.colorTheme": "Default Dark+",
"search.mode": "newEditor",
"editor.renderWhitespace": "boundary",
"intelephense.format.enable": false
} I remembered that I have an extension that resets So my guess is that something in the change for #209038 causes JS/TS to not have the issue that this PR is about. I worry about bringing that up because I really don't want the change to auto-indent after |
Thanks for the reply. So you are saying that without this extension the issue does not happen with JS and TS, and that we could port this same change to PHP and other languages so this does not happens anymore in those languages? I see two changes to port:
I would gladly accept a PR for both of these changes, or if you'd like I'll create an issue to myself about this. |
@aiday-mar The extension is not (yet) available for vscode but here is the full config for the issue described in #136593 (comment): {
"comments": {
"lineComment": "#"
},
"brackets": [
["[", "]"],
["(", ")"],
["{", "}"]
],
"autoClosingPairs": [
{ "open": "[", "close": "]" },
{ "open": "{", "close": "}" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string"] },
{ "open": "%", "close": "%", "notIn": ["string", "comment"] },
],
"autoCloseBefore": ";:.,=}])>` \n\t",
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"],
["%", "%"],
],
"folding": {
"markers": {
"start": "^\\s*#\\s*region\\b",
"end": "^\\s*#\\s*endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~|%\\!\\@\\#\\%\\^\\$\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\<\\>\\/\\?\\s]+)",
"indentationRules": {
"increaseIndentPattern": {
"pattern": "^[^#]*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)(?:#|$)"
},
"decreaseIndentPattern": {
"pattern": "^\\s*[\\}\\)\\]].*$"
}
},
"onEnterRules": [
// Roxygen comments.
{
"beforeText": "^\\s*#+'",
"action": { "indent": "none", "appendText": "#' " }
},
// Plumber comments.
{
"beforeText": "^\\s*#+\\*",
"action": { "indent": "none", "appendText": "#* " }
},
// Regular comments.
// This must come *after* the Roxygen and Plumber rules in this file.
{
"beforeText": "^\\s*#.*",
"afterText": ".+$",
"action": { "indent": "none", "appendText": "# " }
},
// A line ending with an operator, preceded by a blank line.
{
"previousLineText": "^\\s*(?:#|$)",
"beforeText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"action": { "indent": "indent" }
},
// A line ending with an operator, preceded by a line not ending with an operator.
{
"previousLineText": "^(?!.*[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$))[^#]+.*$",
"beforeText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"action": { "indent": "indent" }
},
// A line not ending with an operator, preceded by a line ending with an operator.
{
"previousLineText": "^\\s*[^#]+[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$)",
"beforeText": "^(?!.*[=\\+\\-\\*?/\\^><!&\\|~%\\$:@\\?]\\s*(?:#|$))[^#]+.*$",
"afterText": "^\\s*(#|$)",
"action": { "indent": "outdent" }
},
// Control flow keywords.
{
"beforeText": "^\\s*(?:if|else|for|while|repeat)\\s*(?:\\(.*?\\))?\\s*(?:#|$)",
"action": { "indent": "indent" }
}
]
} I hope that a comprehensive fix for this issue can be implemented. (The more radical and modern fix would be to switch to indentation based on tree-sitter queries, which I hope is on the horizon?) |
Hi @lionel- yes this item is on the horizon, I don't know exactly when this will be implemented but I look forward to doing this as I think it would significantly improve the auto-indentation experience |
@aiday-mar oh that's great news! In that case I'd personally be fine with waiting for the new TS indentation support instead of patching up the current regex engine and making things even more complicated on your side. We have a workaround in place (our LSP uses format-on-type for |
Great thanks! |
Yes, my guess is that it would solve this issue, but I personally don't feel like it's the solution to the root cause. It would solve this particular case, but the following logic seems right to me (copied from above, but I fixed a typo... should have been "non-blank line"):
My guess is that this issue will come up in various languages and they can probably be fixed one by one, but checking I worry slightly about changing default behaviors for indentation, particularly when there's no setting to disable it. Changing the language configuration I think is something that can't be made into a setting. For example, I wish I could disable TS/JS auto-indenting after an Let me know how you want to proceed here. If you want me to try to work on updating the PHP configuration file, I can give it a shot. One final note: a very popular PHP extension updates the PHP language configuration here. So even if the config is updated in VS Code, it probably doesn't actually solve the issue for lots of people. |
Hi @ssigwart thank you for your comment. I think we should update the language configuration file first, through a minimal change. Generally when possible we want to make small changes to the language configuration file and make bigger code changes when we see that the language configuration file can not solve our use case. Following this, we should think about the cases where indentation is not correct (maybe compile them), then work on a PR which uses the Concerning what you mentioned that you'd like disable auto-indentation rules there is a feature request ask to make the indentation rules configurable in the settings file directly (by defining oneself the rules there directly). If this is implemented, users could use this to overwrite indentation rules. I need to discuss this with my manager. |
Thanks, @aiday-mar. I opened #251465 with your suggestions. I thought it would be cleaned to create a new PR than update this one. |
Hi thanks for the PR, I approved it. I also just wanted to let you know that we'll soon be working on tree-sitter based auto-indentation. When that is implemented a lot of auto-indentation bugs should be fixed. I wonder therefore if we should invest more time to work on |
This PR fixes #136592. However, the single line
if
fix only works after #136577 is merged.To test, you can use this:
./scripts/test.sh --glob **/cursor.test.js --grep 136592