Repository: microsoft/vscode (TypeScript)
Date: 2021-09-18 03:40:23+00:00
Task type: Bug Fix

Issue:
[folding] Confusing behavior of Go to next fold
Issue Type: <b>Bug</b>

1. With a JS file:

```js
class Foo {
        // cursor here

	b(){
            // fold this
		
	}

	c() {

	}
}
```

Fold `b` and place cursor after the opening `{` of the class

2. Run `go to next fold`

**Bug**
Nothing happens

VS Code version: Code - Insiders 1.61.0-insider (Universal) (f66a3e06bcb9f000e5dc0ad0040ff9b32fc75c78, 2021-09-14T05:14:54.400Z)
OS version: Darwin x64 20.6.0
Restricted Mode: No

<details>
<summary>System Info</summary>

|Item|Value|
|---|---|
|CPUs|Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz (16 x 2400)|
|GPU Status|2d_canvas: enabled<br>gpu_compositing: enabled<br>metal: disabled_off<br>multiple_raster_threads: enabled_on<br>oop_rasterization: enabled<br>opengl: enabled_on<br>rasterization: enabled<br>skia_renderer: disabled_off_ok<br>video_decode: enabled<br>webgl: enabled<br>webgl2: enabled|
|Load (avg)|3, 2, 2|
|Memory (System)|32.00GB (0.18GB free)|
|Process Argv|--crash-reporter-id 48781ca2-1705-4f64-9bab-325055aab55d|
|Screen Reader|no|
|VM|0%|
</details><details><summary>Extensions (55)</summary>

Extension|Author (truncated)|Version
---|---|---
salesforce-soql-editor|all|1.8.0
comment-tagged-templates|bie|0.3.1
docs-view|bie|0.0.9
emojisense|bie|0.9.0
folder-source-actions|bie|0.2.1
jsdoc-markdown-highlighting|bie|0.0.1
markdown-checkbox|bie|0.3.2
markdown-emoji|bie|0.2.1
markdown-yaml-preamble|bie|0.1.0
test-ext|bie|0.0.1
vscode-eslint|dba|2.1.25
typescript-notebook|don|2.0.3
gitlens|eam|11.6.0
tsl-problem-matcher|eam|0.4.0
EditorConfig|Edi|0.16.4
codespaces|Git|1.0.5
vscode-pull-request-github-insiders|Git|2021.9.19266
vscode-drawio|hed|1.6.2
vscode-postfix-ts|ipa|1.9.4
latex-workshop|Jam|8.20.2
vscode-platform-specific-sample|joa|1.15.0
vscode-platform-specific-sample|joa|1.5.0
vscode-memfs|jri|0.0.3
template-string-converter|meg|0.5.3
vscode-azurefunctions|ms-|1.5.1
vscode-azureresourcegroups|ms-|0.4.0
vscode-docker|ms-|1.16.1
vscode-language-pack-de|MS-|1.60.3
vscode-edge-devtools|ms-|1.3.0
python|ms-|2021.10.1215685612-dev
vscode-pylance|ms-|2021.9.2-pre.1
jupyter|ms-|2021.9.1001232960
jupyter-keymap|ms-|1.0.0
remote-containers|ms-|0.194.0
remote-ssh|ms-|0.65.7
remote-ssh-edit|ms-|0.65.7
azure-account|ms-|0.9.8
hexeditor|ms-|1.8.2
js-debug-nightly|ms-|2021.9.417
live-server|ms-|0.2.8
vscode-github-issue-notebooks|ms-|0.0.106
vscode-typescript-next|ms-|4.5.20210913
vsliveshare|ms-|1.0.4836
debugger-for-chrome|msj|4.13.0
vetur|oct|0.34.1
java|red|0.81.0
code-spell-checker|str|2.0.4
rest-book|tan|4.2.0
ayu|tea|1.0.5
luna-paint|Tyr|0.9.0
sort-lines|Tyr|1.9.0
vscodeintellicode|Vis|1.2.14
vscode-java-debug|vsc|0.35.0
vscode-java-dependency|vsc|0.18.7
vscode-java-test|vsc|0.31.3

(2 theme extensions excluded)

</details><details>
<summary>A/B Experiments</summary>

```
vsliv695:30137379
vsins829:30139715
vsliv368cf:30146710
vsreu685:30147344
python383:30185418
pythonvspyt602:30291494
vspor879:30202332
vspor708:30202333
vspor363:30204092
pythonvspyt639:30291487
pythontb:30258533
pythonvspyt551cf:30291413
pythonptprofiler:30281269
vshan820:30294714
pythondataviewer:30285072
pythonvsuse255:30319630
vscod805cf:30301675
pythonvspyt200:30323110
vsccppwt:30364496
pythonvssor306:30340298
bridge0708:30335490
pygetstartedt2:30353727
dockerwalkthru:30364418
bridge0723:30353136
pythonrunftest32:30365365
pythonf5test824:30361779
javagetstartedt:30350119
pythonvspyt187:30365360
pydsgst2:30361790
vssur157:30365996

```

</details>

<!-- generated by issue reporter -->


Issue discussion:
I'm trying to document this command but find its behavior difficult to understand and rather unpredictable 

This could be wrong, but I think it tries looking for the next fold above the current level. In the case above, that means that `go to next fold` works if you are inside another method:

```js
class Foo {
	a() {
		// Jumping to folded `b` works from here
	}

     // But not from here

	b(){
	  // fold this
	}
}
```

But it doesn't work if you are in the class body

I'd expect this command to work more like `go to next problem` where it just jumps to the next folded location in the file
FYI @ssigwart
Got to next fold looks for the next fold of the same level. In the example above there's no such fold
Thanks for tagging me, @aeschli.  The way I have fold jumping working is that it takes the cursor position and figures out what fold you are currently in.  From there, it checks if you want the next previous or parent fold.  Parent is pretty easy.  It just gets the fold that's at level X - 1.  For next and previous fold, they are implemented as the next or previous fold _at the same level_.  The reason for that is because it makes it easy to find associated parts of the file.  For example, if you are at an `if`, `else if`, or `else` fold, you can easily navigate to the other `if` conditions.  Similarly, in the above example, if you are on function `a`, you can easily jump to the other functions that are defined.  I don't think it's valuable to jump to the next or previous fold by file order.  For example, in the below code it would not make sense to me to have jump to next fold go to b1, then c1, then c2, then a2.
```javascript
if (a1 === 1) // Cursor here
{
	if (b1 === 1)
	{
		if (c1 === 1)
			console.log('abc');
		else if (c2 === 2)
			console.log('abc');
	}
}
else if (a2 === 2)
{
}
else if (a3 === 3)
{
}
```

Please let me know if you want me to make any changes though.
We need a better label then: `Go to Next Sibling Fold` ?


That seems reasonable.  Want me to do that?  I probably shouldn't change the actually command ID though, right?  That would break key bindings.
Thanks for the explanation. I agree that this should be renamed. 

Even with a better name though, I still find the behavior from my example confusing. When the cursor is inside the class body, I'd expect that the folding points for `b` and `c` would be siblings to the cursor's location (because they all have the same folding parent). Instead though we jump out of the class entirely

One other note on the name: maybe the command should call these something like `folding points` instead of `fold`? Conceptually I think of a `fold` as being code you've actually folded, but my understanding is that these commands actually jump between foldable lines in the file
`folding points` sounds good to me.

I see how that's confusing, so I put together this JS sample to illustrate where the "next fold" is for different cursor positions:
```javascript
class Foo1 {
	// cursor 1

	b() { // cursor 2
		return; // cursor 3
	}
	// cursor 4
	c() { // cursor 2 next; cursor 3 next
		return;
	}
	// cursor 5
}

class Foo2 { // cursor 1 next; cursor 4 next; cursor 5 next
	b() {
		return;
	}

	c() {
		return;
	}
}
```

- Cursor 2's next is straightforward since you're on a folding point.
- Cursor 3's next I think is reasonable.  You are in a fold, so you find the folding point and go to the next of that.
- Cursor 1 and 5's next start getting a little weird, but following the same logic as cursor 3, it seems logical that `Foo1` is the folding point, so the next sibling is at `Foo2`.
- Cursor 4 is basically your original question.  I agree that it gets a little confusing.  The logic is the same as 1, 3, and 5, but visually, if you're between 2 folding points, I could understand the desire for it to go to those points.

I think the confusion stems from using the command when you're not on a folding point line.  If you are, I think it's easy to understand, but when you aren't, not so much.  What do you think of the following?

1.  If you are on a folding point line, do what it does now.
2. If not, go to the next folding point sequentially.

With those updates, the above example looks like this:
```javascript
class Foo1 {
	// cursor 1

	b() { // cursor 2; cursor 1 next
		return; // cursor 3
	}
	// cursor 4
	c() { // cursor 2 next; cursor 3 next, cursor 4 next
		return;
	}
	// cursor 5
}

class Foo2 { // cursor 5 next
	b() {
		return;
	}

	c() {
		return;
	}
}
```
We don't use 'folding points' as a term. I suggest 'folding range'
Ok.  What do you think of changing the implementation as I mentioned in my previous comment?
Yes I like the suggestion (I thumbed up it :-) )

Solution patch:
diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts
--- a/src/vs/editor/contrib/folding/folding.ts
+++ b/src/vs/editor/contrib/folding/folding.ts
@@ -1002,8 +1002,8 @@ class GotoPreviousFoldAction extends FoldingAction<void> {
 	constructor() {
 		super({
 			id: 'editor.gotoPreviousFold',
-			label: nls.localize('gotoPreviousFold.label', "Go to Previous Fold"),
-			alias: 'Go to Previous Fold',
+			label: nls.localize('gotoPreviousFold.label', "Go to Previous Folding Range"),
+			alias: 'Go to Previous Folding Range',
 			precondition: CONTEXT_FOLDING_ENABLED,
 			kbOpts: {
 				kbExpr: EditorContextKeys.editorTextFocus,
@@ -1033,8 +1033,8 @@ class GotoNextFoldAction extends FoldingAction<void> {
 	constructor() {
 		super({
 			id: 'editor.gotoNextFold',
-			label: nls.localize('gotoNextFold.label', "Go to Next Fold"),
-			alias: 'Go to Next Fold',
+			label: nls.localize('gotoNextFold.label', "Go to Next Folding Range"),
+			alias: 'Go to Next Folding Range',
 			precondition: CONTEXT_FOLDING_ENABLED,
 			kbOpts: {
 				kbExpr: EditorContextKeys.editorTextFocus,
diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts
--- a/src/vs/editor/contrib/folding/foldingModel.ts
+++ b/src/vs/editor/contrib/folding/foldingModel.ts
@@ -454,7 +454,8 @@ export function getParentFoldLine(lineNumber: number, foldingModel: FoldingModel
  */
 export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingModel): number | null {
 	let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
-	if (foldingRegion !== null) {
+	// If on the folding range start line, go to previous sibling.
+	if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
 		// If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold.
 		if (lineNumber !== foldingRegion.startLineNumber) {
 			return foldingRegion.startLineNumber;
@@ -487,8 +488,8 @@ export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingMod
 		if (foldingModel.regions.length > 0) {
 			foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1);
 			while (foldingRegion !== null) {
-				// Found non-parent fold before current line.
-				if (foldingRegion.parentIndex === -1 && foldingRegion.startLineNumber < lineNumber) {
+				// Found fold before current line.
+				if (foldingRegion.startLineNumber < lineNumber) {
 					return foldingRegion.startLineNumber;
 				}
 				if (foldingRegion.regionIndex > 0) {
@@ -511,7 +512,8 @@ export function getPreviousFoldLine(lineNumber: number, foldingModel: FoldingMod
  */
 export function getNextFoldLine(lineNumber: number, foldingModel: FoldingModel): number | null {
 	let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
-	if (foldingRegion !== null) {
+	// If on the folding range start line, go to next sibling.
+	if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
 		// Find max line number to stay within parent.
 		let expectedParentIndex = foldingRegion.parentIndex;
 		let maxLineNumber = 0;
@@ -543,8 +545,8 @@ export function getNextFoldLine(lineNumber: number, foldingModel: FoldingModel):
 		if (foldingModel.regions.length > 0) {
 			foldingRegion = foldingModel.regions.toRegion(0);
 			while (foldingRegion !== null) {
-				// Found non-parent fold after current line.
-				if (foldingRegion.parentIndex === -1 && foldingRegion.startLineNumber > lineNumber) {
+				// Found fold after current line.
+				if (foldingRegion.startLineNumber > lineNumber) {
 					return foldingRegion.startLineNumber;
 				}
 				if (foldingRegion.regionIndex < foldingModel.regions.length) {


Test patch:
diff --git a/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/foldingModel.test.ts
--- a/src/vs/editor/contrib/folding/test/foldingModel.test.ts
+++ b/src/vs/editor/contrib/folding/test/foldingModel.test.ts
@@ -875,12 +875,19 @@ suite('Folding Model', () => {
 			assert.strictEqual(getPreviousFoldLine(9, foldingModel), 5);
 			assert.strictEqual(getPreviousFoldLine(5, foldingModel), 3);
 			assert.strictEqual(getPreviousFoldLine(3, foldingModel), null);
+			// Test when not on a folding region start line.
+			assert.strictEqual(getPreviousFoldLine(4, foldingModel), 3);
+			assert.strictEqual(getPreviousFoldLine(7, foldingModel), 6);
+			assert.strictEqual(getPreviousFoldLine(8, foldingModel), 6);
 
 			// Test jump to next.
 			assert.strictEqual(getNextFoldLine(3, foldingModel), 5);
-			assert.strictEqual(getNextFoldLine(4, foldingModel), 5);
 			assert.strictEqual(getNextFoldLine(5, foldingModel), 9);
 			assert.strictEqual(getNextFoldLine(9, foldingModel), null);
+			// Test when not on a folding region start line.
+			assert.strictEqual(getNextFoldLine(4, foldingModel), 5);
+			assert.strictEqual(getNextFoldLine(7, foldingModel), 9);
+			assert.strictEqual(getNextFoldLine(8, foldingModel), 9);
 
 		} finally {
 			textModel.dispose();