Repository: microsoft/vscode (TypeScript)
Date: 2023-03-30 15:10:19+00:00
Task type: Feature

Issue:
Use only unique commit messages for auto-complete
<!-- ⚠️⚠️ 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.76.2 (user setup)
OS: Windows_NT x64 10.0.19045

When using the integrated source control we can loop through commit messages that were used for previously committed files, which is very handy indeed but when we had committed a file with the same commit message, all of the (same) commit messages are used for auto-completion, making the feature less useful and more time-consuming, see example below.

Wouldn't be better to only show unique commit messages? That would be faster to store, process, display and loop through. 😀

`includes`/`indexOf` or `Set` are the first thing that comes to my mind.

<br>
<br>

<div align="center">
  <video src="https://user-images.githubusercontent.com/20957750/228090529-cc70ac12-420a-41c9-b20b-f7527e78b7b7.mp4">
</div>

<br>
<br>

\* Note: I only use these non-meaningful commit messages for READMEs. 😇

<br>
<br>

Steps to Reproduce:

1. Commit a file with the same commit message multiple times
2. Scroll through the commit messages, you should see multiple identical messages



Issue discussion:

Solution patch:
diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts
--- a/src/vs/base/common/history.ts
+++ b/src/vs/base/common/history.ts
@@ -118,6 +118,7 @@ interface HistoryNode<T> {
 
 export class HistoryNavigator2<T> {
 
+	private valueSet: Set<T>;
 	private head: HistoryNode<T>;
 	private tail: HistoryNode<T>;
 	private cursor: HistoryNode<T>;
@@ -135,6 +136,7 @@ export class HistoryNavigator2<T> {
 			next: undefined
 		};
 
+		this.valueSet = new Set<T>([history[0]]);
 		for (let i = 1; i < history.length; i++) {
 			this.add(history[i]);
 		}
@@ -152,7 +154,15 @@ export class HistoryNavigator2<T> {
 		this.cursor = this.tail;
 		this.size++;
 
+		if (this.valueSet.has(value)) {
+			this._deleteFromList(value);
+		} else {
+			this.valueSet.add(value);
+		}
+
 		while (this.size > this.capacity) {
+			this.valueSet.delete(this.head.value);
+
 			this.head = this.head.next!;
 			this.head.previous = undefined;
 			this.size--;
@@ -163,8 +173,20 @@ export class HistoryNavigator2<T> {
 	 * @returns old last value
 	 */
 	replaceLast(value: T): T {
+		if (this.tail.value === value) {
+			return value;
+		}
+
 		const oldValue = this.tail.value;
+		this.valueSet.delete(oldValue);
 		this.tail.value = value;
+
+		if (this.valueSet.has(value)) {
+			this._deleteFromList(value);
+		} else {
+			this.valueSet.add(value);
+		}
+
 		return oldValue;
 	}
 
@@ -193,14 +215,7 @@ export class HistoryNavigator2<T> {
 	}
 
 	has(t: T): boolean {
-		let temp: HistoryNode<T> | undefined = this.head;
-		while (temp) {
-			if (temp.value === t) {
-				return true;
-			}
-			temp = temp.next;
-		}
-		return false;
+		return this.valueSet.has(t);
 	}
 
 	resetCursor(): T {
@@ -216,4 +231,24 @@ export class HistoryNavigator2<T> {
 			node = node.next;
 		}
 	}
+
+	private _deleteFromList(value: T): void {
+		let temp = this.head;
+
+		while (temp !== this.tail) {
+			if (temp.value === value) {
+				if (temp === this.head) {
+					this.head = this.head.next!;
+					this.head.previous = undefined;
+				} else {
+					temp.previous!.next = temp.next;
+					temp.next!.previous = temp.previous;
+				}
+
+				this.size--;
+			}
+
+			temp = temp.next!;
+		}
+	}
 }


Test patch:
diff --git a/src/vs/base/test/common/history.test.ts b/src/vs/base/test/common/history.test.ts
--- a/src/vs/base/test/common/history.test.ts
+++ b/src/vs/base/test/common/history.test.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 import * as assert from 'assert';
-import { HistoryNavigator } from 'vs/base/common/history';
+import { HistoryNavigator, HistoryNavigator2 } from 'vs/base/common/history';
 
 suite('History Navigator', () => {
 
@@ -176,3 +176,95 @@ suite('History Navigator', () => {
 		return result;
 	}
 });
+
+suite('History Navigator 2', () => {
+
+	test('constructor', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+
+		assert.strictEqual(testObject.current(), '4');
+		assert.strictEqual(testObject.isAtEnd(), true);
+	});
+
+	test('constructor - initial history is not empty', () => {
+		assert.throws(() => new HistoryNavigator2([]));
+	});
+
+	test('constructor - capacity limit', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4'], 3);
+
+		assert.strictEqual(testObject.current(), '4');
+		assert.strictEqual(testObject.isAtEnd(), true);
+		assert.strictEqual(testObject.has('1'), false);
+	});
+
+	test('constructor - duplicate values', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4', '3', '2', '1']);
+
+		assert.strictEqual(testObject.current(), '1');
+		assert.strictEqual(testObject.isAtEnd(), true);
+	});
+
+	test('navigation', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+
+		assert.strictEqual(testObject.current(), '4');
+		assert.strictEqual(testObject.isAtEnd(), true);
+
+		assert.strictEqual(testObject.next(), '4');
+		assert.strictEqual(testObject.previous(), '3');
+		assert.strictEqual(testObject.previous(), '2');
+		assert.strictEqual(testObject.previous(), '1');
+		assert.strictEqual(testObject.previous(), '1');
+
+		assert.strictEqual(testObject.current(), '1');
+		assert.strictEqual(testObject.next(), '2');
+		assert.strictEqual(testObject.resetCursor(), '4');
+	});
+
+	test('add', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+		testObject.add('5');
+
+		assert.strictEqual(testObject.current(), '5');
+		assert.strictEqual(testObject.isAtEnd(), true);
+	});
+
+	test('add - existing value', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+		testObject.add('2');
+
+		assert.strictEqual(testObject.current(), '2');
+		assert.strictEqual(testObject.isAtEnd(), true);
+
+		assert.strictEqual(testObject.previous(), '4');
+		assert.strictEqual(testObject.previous(), '3');
+		assert.strictEqual(testObject.previous(), '1');
+	});
+
+	test('replaceLast', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+		testObject.replaceLast('5');
+
+		assert.strictEqual(testObject.current(), '5');
+		assert.strictEqual(testObject.isAtEnd(), true);
+		assert.strictEqual(testObject.has('4'), false);
+
+		assert.strictEqual(testObject.previous(), '3');
+		assert.strictEqual(testObject.previous(), '2');
+		assert.strictEqual(testObject.previous(), '1');
+	});
+
+	test('replaceLast - existing value', () => {
+		const testObject = new HistoryNavigator2(['1', '2', '3', '4']);
+		testObject.replaceLast('2');
+
+		assert.strictEqual(testObject.current(), '2');
+		assert.strictEqual(testObject.isAtEnd(), true);
+		assert.strictEqual(testObject.has('4'), false);
+
+		assert.strictEqual(testObject.previous(), '3');
+		assert.strictEqual(testObject.previous(), '1');
+	});
+
+});