Skip to content

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

ssigwart
Copy link
Contributor

@ssigwart ssigwart commented Nov 7, 2021

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

@ssigwart
Copy link
Contributor Author

ssigwart commented Jun 5, 2022

I rebased this to fix a merge conflict.

@ssigwart
Copy link
Contributor Author

@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!

@lionel-
Copy link

lionel- commented Mar 29, 2024

@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:

foo &&
    bar<CURSOR>

Where enter correctly dedents the first time:

foo &&
    bar
<CURSOR>

But on subsequent times enter suddenly reindents:

foo &&
    bar

    <CURSOR>

@aiday-mar
Copy link
Contributor

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 indentNextLinePattern of the language configuration file, which will specify that after the line ending with bar indentation should be applied only on the next line. What language are you currently working with?

@lionel-
Copy link

lionel- commented Apr 3, 2024

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 indentNextLinePattern, this will not work in our case. We can't use an increase-indent pattern for lines ending with an operator here, even one that only works on the next line as you suggested, because they produce this kind of staircase indentation:

increase-indent-rule.mov

To avoid this, we are using OnEnter rules that increase indentation when entering a chain of operators and decrease it when the chain is finished:

	"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.mov

Unfortunately the "outdent" action is not sticky, causing the following indentation bug on ulterior lines:

outdent-stickiness-bug.mov

We have verified that the PR fixes the non-stickiness of OnEnter outdent rules for our use case.

@lionel-
Copy link

lionel- commented Apr 4, 2024

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>

@aiday-mar
Copy link
Contributor

aiday-mar commented Apr 5, 2024

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.

@lionel-
Copy link

lionel- commented Apr 9, 2024

While testing this branch we noticed a problem in the way the afterText matcher is created here:

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 bar() call is the last line in the file and move the cursor to the end. Type Enter twice, only the first one works.

@ssigwart
Copy link
Contributor Author

ssigwart commented Apr 9, 2024

@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.

@lionel-
Copy link

lionel- commented Apr 9, 2024

@ssigwart

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 type() routine, interrupting the insertion of \n.

@ssigwart
Copy link
Contributor Author

ssigwart commented Apr 9, 2024

@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.

@aiday-mar aiday-mar requested review from alexdima and aiday-mar and removed request for alexdima October 7, 2024 07:35
@vivodi
Copy link

vivodi commented Dec 29, 2024

Is this still relevant?

@ssigwart
Copy link
Contributor Author

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.

@aiday-mar
Copy link
Contributor

aiday-mar commented May 7, 2025

So looking at the comments here it seems to me that the main issue is that when pressing Enter from an empty line, the next line is indented to have the same indent as the last non-empty line. Maybe instead of computing the onEnter result when honoring indentation, we could check if the current line is empty or consists only of whitespaces and if yes just insert the same indentation as for the current line. This would be a simpler fix and does not require fetching the onEnter rules. I am still debating however if this should be done and need to think some more.

@aiday-mar aiday-mar assigned aiday-mar and unassigned rebornix May 7, 2025
@ssigwart
Copy link
Contributor Author

ssigwart commented May 7, 2025

Maybe instead of computing the onEnter result when honoring indentation, we could check if the current line is empty or consists only of whitespaces and if yes just insert the same indentation as for the current line.

That sounds reasonable to me.

Copy link
Contributor

@aiday-mar aiday-mar left a 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:

  1. 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.
  2. 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?
  3. 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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@aiday-mar aiday-mar added changes-requested editor-autoindent Editor auto indentation issues labels Jun 5, 2025
Copy link
Contributor Author

@ssigwart ssigwart left a 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);
Copy link
Contributor Author

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.

Comment on lines +82 to +86
try {
afterEnterText = model.getLineContent(currentLineNumber);
} catch (e) {
// Probably after the end of file
}
Copy link
Contributor Author

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.

@aiday-mar
Copy link
Contributor

Great thanks I will have a look again

@aiday-mar
Copy link
Contributor

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:

/**
 * PHPDoc
 */

The editor adds a new line on the next line, and that after:

if (1)
	1;

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 onEnter rules?

If we go forward with considering onEnter rules when determining indentation, I think we should maybe instead make the change at a lower level not just in the code that determines the previous valid indentation. To see what I mean please have a look at the code in the PR here: https://github.com/microsoft/vscode/pull/250854/files which changes the IndentRulesSupport class. This PR is partly based on the PR made here by @ssigwart. It checks the onEnter rules when determining whether to indent/outdent as follows, for example:

	public shouldIncrease(autoIndent: EditorAutoIndentStrategy, useOnEnterRulesForInheritedIndent: boolean, text: string): boolean {
		if (this._indentationRules) {
			if (this._indentationRules.increaseIndentPattern && resetGlobalRegex(this._indentationRules.increaseIndentPattern) && this._indentationRules.increaseIndentPattern.test(text)) {
				return true;
			}
		}
		if (this._onEnterSupport && useOnEnterRulesForInheritedIndent) {
			const enterResult = this._onEnterSupport.onEnter(autoIndent, '', '', text);
			if (enterResult && enterResult.indentAction === IndentAction.Indent) {
				return true;
			}
		}
		return false;
	}

I am currently not sure if we should proceed with this PR however.

@ssigwart
Copy link
Contributor Author

@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 | is below:

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 onEnter tells you what indent to have on the next line and that works. However, when you hit enter on a blank line, it finds the previous blank line and uses the indentation of that line. So it's like hitting enter after that line and therefore the onEnter rules seemed appropriate.

@aiday-mar
Copy link
Contributor

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

@ssigwart
Copy link
Contributor Author

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 extensions/javascript/javascript-language-configuration.json to prior to #209038 because I didn't agree that the editor should auto-indent after if (see #43244 (comment) and #43244 (comment)). If I have that extension enabled, I see the issue in JavaScript and Typescript, but not if I don't.

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 if brought over to PHP.

@aiday-mar
Copy link
Contributor

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:

  1. The one which indents the next line after a braceless if statement
  2. The one that remove text after the end of a block comment

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.

@lionel-
Copy link

lionel- commented Jun 12, 2025

@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?)

@aiday-mar
Copy link
Contributor

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

@lionel-
Copy link

lionel- commented Jun 12, 2025

@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 \n characters to fix up incorrect indentation - this causes visible indentation changes so the TS support will be welcome, but until then this works well enough in practice).

@aiday-mar
Copy link
Contributor

aiday-mar commented Jun 12, 2025

Great thanks!

@ssigwart
Copy link
Contributor Author

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:

  1. The one which indents the next line after a braceless if statement
  2. The one that remove text after the end of a block comment

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.

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 thought in this PR was that onEnter tells you what indent to have on the next line and that works. However, when you hit enter on a blank line, it finds the previous non-blank line and uses the indentation of that line. So it's like hitting enter after that line and therefore the onEnter rules seemed appropriate.

My guess is that this issue will come up in various languages and they can probably be fixed one by one, but checking onEnter feels right to me. That said, I'm not working in the VS Code code all the time, so I don't have as good of a knowledge of this.

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 if. If there was eventually a way to customize settings like that, that would be good.

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.

@aiday-mar
Copy link
Contributor

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 onEnter rules behind a setting, do some testing and validate that the PR fixes these incorrect indentation cases and does not introduce new bugs.

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.

@ssigwart
Copy link
Contributor Author

Thanks, @aiday-mar. I opened #251465 with your suggestions. I thought it would be cleaned to create a new PR than update this one.

@aiday-mar
Copy link
Contributor

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 onEnter rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
editor-autoindent Editor auto indentation issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

onEnterRules not considered when determining new line indentation
5 participants