Repository: microsoft/vscode (TypeScript) — Bug Fix

Issue:
Sticky scroll doesn't work with C++ 17 nested namespaces
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.71.0-insider, Commit: 9529e11f6481
- OS Version: macOS 12.5.1 (21G83)

Steps to Reproduce:

Sticky scroll does not work correctly for a file like this:

```c++
namespace A::B {

void func() {
  //
  //
  //
  //
}

} // namespace A::B
```

Scrolling into the body of func() shows a sticky scroll area containing only `namespace A::B {`. I expect two lines instead, like this:
```
namespace A::B {
void func() {
```

However, it _does_ work for a C++ file like this:

```c++
namespace A {
namespace B {

void func() {
  //
  //
  //
  //
}

} // namespace B
} // namespace A
```

In this case, scrolling into the body of func() shows a sticky scroll area containing three lines, as expected:
```
namespace A {
namespace B {
void func() {
```



Discussion:

Resolving patch:
diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts
--- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts
+++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts
@@ -92,7 +92,7 @@ export class StickyLineCandidateProvider extends Disposable {
 			if (token.isCancellationRequested) {
 				return;
 			}
-			this._outlineModel = StickyOutlineElement.fromOutlineModel(outlineModel);
+			this._outlineModel = StickyOutlineElement.fromOutlineModel(outlineModel, -1);
 			this._modelVersionId = modelVersionId;
 		}
 	}
@@ -158,12 +158,20 @@ export class StickyLineCandidateProvider extends Disposable {
 }
 
 class StickyOutlineElement {
-	public static fromOutlineModel(outlineModel: OutlineModel | OutlineElement | OutlineGroup): StickyOutlineElement {
+	public static fromOutlineModel(outlineModel: OutlineModel | OutlineElement | OutlineGroup, previousStartLine: number): StickyOutlineElement {
 
 		const children: StickyOutlineElement[] = [];
 		for (const child of outlineModel.children.values()) {
-			if (child instanceof OutlineElement && child.symbol.selectionRange.startLineNumber !== child.symbol.range.endLineNumber || child instanceof OutlineGroup || child instanceof OutlineModel) {
-				children.push(StickyOutlineElement.fromOutlineModel(child));
+			if (child instanceof OutlineGroup || child instanceof OutlineModel) {
+				children.push(StickyOutlineElement.fromOutlineModel(child, previousStartLine));
+			} else if (child instanceof OutlineElement && child.symbol.selectionRange.startLineNumber !== child.symbol.range.endLineNumber) {
+				if (child.symbol.selectionRange.startLineNumber !== previousStartLine) {
+					children.push(StickyOutlineElement.fromOutlineModel(child, child.symbol.selectionRange.startLineNumber));
+				} else {
+					for (const subchild of child.children.values()) {
+						children.push(StickyOutlineElement.fromOutlineModel(subchild, child.symbol.selectionRange.startLineNumber));
+					}
+				}
 			}
 		}
 		children.sort((child1, child2) => {


Test patch:
diff --git a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts
--- a/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts
+++ b/src/vs/editor/contrib/stickyScroll/test/browser/stickyScroll.test.ts
@@ -12,11 +12,13 @@ import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeatu
 import { DocumentSymbol, SymbolKind } from 'vs/editor/common/languages';
 import { StickyLineCandidate, StickyLineCandidateProvider } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollProvider';
 import { EditorOption } from 'vs/editor/common/config/editorOptions';
+import { ILogService, NullLogService } from 'vs/platform/log/common/log';
 
 suite('Sticky Scroll Tests', () => {
 
 	const serviceCollection = new ServiceCollection(
-		[ILanguageFeaturesService, new LanguageFeaturesService()]
+		[ILanguageFeaturesService, new LanguageFeaturesService()],
+		[ILogService, new NullLogService()]
 	);
 
 	const text = [
@@ -121,10 +123,10 @@ suite('Sticky Scroll Tests', () => {
 		await withAsyncTestCodeEditor(model, { serviceCollection }, async (editor, _viewModel, instantiationService) => {
 
 			const stickyScrollController: StickyScrollController = editor.registerAndInstantiateContribution(StickyScrollController.ID, StickyScrollController);
-			await stickyScrollController.stickyScrollCandidateProvider.update();
 			const lineHeight: number = editor.getOption(EditorOption.lineHeight);
 			const languageService: ILanguageFeaturesService = instantiationService.get(ILanguageFeaturesService);
 			languageService.documentSymbolProvider.register('*', documentSymbolProviderForTestModel());
+			await stickyScrollController.stickyScrollCandidateProvider.update();
 			let state;
 
 			editor.setScrollTop(1);
@@ -163,12 +165,11 @@ suite('Sticky Scroll Tests', () => {
 		await withAsyncTestCodeEditor(model, { serviceCollection }, async (editor, viewModel, instantiationService) => {
 
 			const stickyScrollController: StickyScrollController = editor.registerAndInstantiateContribution(StickyScrollController.ID, StickyScrollController);
-			await stickyScrollController.stickyScrollCandidateProvider.update();
 			const lineHeight = editor.getOption(EditorOption.lineHeight);
 
 			const languageService = instantiationService.get(ILanguageFeaturesService);
 			languageService.documentSymbolProvider.register('*', documentSymbolProviderForTestModel());
-
+			await stickyScrollController.stickyScrollCandidateProvider.update();
 			editor.setHiddenAreas([{ startLineNumber: 2, endLineNumber: 2, startColumn: 1, endColumn: 1 }, { startLineNumber: 10, endLineNumber: 11, startColumn: 1, endColumn: 1 }]);
 			let state;
 
@@ -197,4 +198,90 @@ suite('Sticky Scroll Tests', () => {
 			model.dispose();
 		});
 	});
+
+	const textWithScopesWithSameStartingLines = [
+		'class TestClass { foo() {',
+		'function bar(){',
+		'',
+		'}}',
+		'}',
+		''
+	].join('\n');
+
+	function documentSymbolProviderForSecondTestModel() {
+		return {
+			provideDocumentSymbols() {
+				return [
+					{
+						name: 'TestClass',
+						detail: 'TestClass',
+						kind: SymbolKind.Class,
+						tags: [],
+						range: { startLineNumber: 1, endLineNumber: 5, startColumn: 1, endColumn: 1 },
+						selectionRange: { startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: 1 },
+						children: [
+							{
+								name: 'foo',
+								detail: 'foo',
+								kind: SymbolKind.Function,
+								tags: [],
+								range: { startLineNumber: 1, endLineNumber: 4, startColumn: 1, endColumn: 1 },
+								selectionRange: { startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: 1 },
+								children: [
+									{
+										name: 'bar',
+										detail: 'bar',
+										kind: SymbolKind.Function,
+										tags: [],
+										range: { startLineNumber: 2, endLineNumber: 4, startColumn: 1, endColumn: 1 },
+										selectionRange: { startLineNumber: 2, endLineNumber: 2, startColumn: 1, endColumn: 1 },
+										children: []
+									} as DocumentSymbol
+								]
+							} as DocumentSymbol,
+						]
+					} as DocumentSymbol
+				];
+			}
+		};
+	}
+
+	test('issue #159271 : render the correct widget state when the child scope starts on the same line as the parent scope', async () => {
+
+		const model = createTextModel(textWithScopesWithSameStartingLines);
+		await withAsyncTestCodeEditor(model, { serviceCollection }, async (editor, _viewModel, instantiationService) => {
+
+			const stickyScrollController: StickyScrollController = editor.registerAndInstantiateContribution(StickyScrollController.ID, StickyScrollController);
+			const lineHeight = editor.getOption(EditorOption.lineHeight);
+
+			const languageService = instantiationService.get(ILanguageFeaturesService);
+			languageService.documentSymbolProvider.register('*', documentSymbolProviderForSecondTestModel());
+			await stickyScrollController.stickyScrollCandidateProvider.update();
+			let state;
+
+			editor.setScrollTop(1);
+			state = stickyScrollController.getScrollWidgetState();
+			assert.deepStrictEqual(state.lineNumbers, [1, 2]);
+
+			editor.setScrollTop(lineHeight + 1);
+			state = stickyScrollController.getScrollWidgetState();
+			assert.deepStrictEqual(state.lineNumbers, [1, 2]);
+
+			editor.setScrollTop(2 * lineHeight + 1);
+			state = stickyScrollController.getScrollWidgetState();
+			assert.deepStrictEqual(state.lineNumbers, [1]);
+
+			editor.setScrollTop(3 * lineHeight + 1);
+			state = stickyScrollController.getScrollWidgetState();
+			assert.deepStrictEqual(state.lineNumbers, [1]);
+
+			editor.setScrollTop(4 * lineHeight + 1);
+			state = stickyScrollController.getScrollWidgetState();
+			assert.deepStrictEqual(state.lineNumbers, []);
+
+			stickyScrollController.dispose();
+			stickyScrollController.stickyScrollCandidateProvider.dispose();
+			model.dispose();
+		});
+	});
 });