| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Common from '../../../core/common/common.js'; |
| import { |
| createFakeSetting, |
| describeWithEnvironment, |
| } from '../../../testing/EnvironmentHelpers.js'; |
| |
| import * as TextEditor from './text_editor.js'; |
| |
| describeWithEnvironment('AutocompleteHistory', () => { |
| let setting: Common.Settings.Setting<string[]>; |
| let history: TextEditor.AutocompleteHistory.AutocompleteHistory; |
| |
| beforeEach(() => { |
| setting = createFakeSetting('history', []); |
| history = new TextEditor.AutocompleteHistory.AutocompleteHistory(setting); |
| }); |
| |
| describe('pushHistoryItem', () => { |
| it('stores the commited text in the setting', () => { |
| history.pushHistoryItem('sample input'); |
| |
| assert.deepEqual(setting.get(), ['sample input']); |
| }); |
| |
| it('ignores sub-sequent identical inputs', () => { |
| history.pushHistoryItem('entry x'); |
| history.pushHistoryItem('entry x'); |
| |
| assert.deepEqual(setting.get(), ['entry x']); |
| }); |
| |
| it('resets the history navigation back to the beginning', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| history.previous(''); |
| history.previous(''); |
| |
| // Even though the user navigated back in the history, they can still commit a different text |
| history.pushHistoryItem('entry 3'); |
| |
| assert.isUndefined(history.next('')); |
| assert.strictEqual(history.previous(''), 'entry 3'); |
| }); |
| }); |
| |
| describe('previous', () => { |
| it('returns "undefined" for an empty history', () => { |
| assert.isUndefined(history.previous('')); |
| }); |
| |
| it('moves backwards through history', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| assert.strictEqual(history.previous(''), 'entry 2'); |
| assert.strictEqual(history.previous(''), 'entry 1'); |
| assert.isUndefined(history.previous('')); |
| }); |
| |
| it('supports navigating backwards with empty current text', () => { |
| history.pushHistoryItem('{a:1, b:2}'); |
| |
| assert.strictEqual(history.previous(''), '{a:1, b:2}'); |
| }); |
| |
| describe('prefix-based history filtering (zsh-style)', () => { |
| it('filters history by prefix when navigating backward', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| history.pushHistoryItem('console.log("b")'); |
| history.pushHistoryItem('document.getElementById("x")'); |
| history.pushHistoryItem('console.log("c")'); |
| |
| // Type "console" and press Up - should only get console.log entries |
| let currentText = 'console'; |
| assert.strictEqual(history.previous(currentText), 'console.log("c")'); |
| currentText = 'console.log("c")'; |
| assert.strictEqual(history.previous(currentText), 'console.log("b")'); |
| currentText = 'console.log("b")'; |
| assert.strictEqual(history.previous(currentText), 'console.log("a")'); |
| currentText = 'console.log("a")'; |
| assert.isUndefined(history.previous(currentText)); // No more matches |
| }); |
| |
| it('returns all entries when prefix is empty', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| history.pushHistoryItem('console.log("b")'); |
| |
| // Empty prefix - should get all entries (existing behavior) |
| assert.strictEqual(history.previous(''), 'console.log("b")'); |
| assert.strictEqual(history.previous(''), 'alert("hello")'); |
| assert.strictEqual(history.previous(''), 'console.log("a")'); |
| }); |
| |
| it('returns all entries when prefix has no matches', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| history.pushHistoryItem('console.log("b")'); |
| |
| // No match for prefix - should fall back to unfiltered history navigation. |
| assert.strictEqual(history.previous('nomatch'), 'console.log("b")'); |
| assert.strictEqual(history.previous('console.log("b")'), 'alert("hello")'); |
| assert.strictEqual(history.previous('alert("hello")'), 'console.log("a")'); |
| assert.isUndefined(history.previous('console.log("a")')); |
| }); |
| |
| it('re-evaluates prefix filtering after commit', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| |
| // Navigate with prefix |
| history.previous('console'); |
| |
| // Commit a new command |
| history.pushHistoryItem('new command'); |
| |
| // Uses the new prefix instead of the previous filtered state. |
| assert.strictEqual(history.previous('alert'), 'alert("hello")'); |
| }); |
| }); |
| |
| }); |
| |
| describe('next', () => { |
| it('returns "undefined" for an empty history', () => { |
| assert.isUndefined(history.next('')); |
| }); |
| |
| it('returns "undefined" when not navigating through the history', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| assert.isUndefined(history.next('')); |
| }); |
| |
| it('moves forwards through history', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| history.previous(''); |
| history.previous(''); |
| history.previous(''); |
| |
| assert.strictEqual(history.next(''), 'entry 1'); |
| assert.strictEqual(history.next(''), 'entry 2'); |
| }); |
| |
| describe('prefix-based history filtering (zsh-style)', () => { |
| it('navigates forward through filtered history', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| history.pushHistoryItem('console.log("b")'); |
| |
| // Navigate back with prefix filter |
| assert.strictEqual(history.previous('console'), 'console.log("b")'); |
| assert.strictEqual(history.previous('console.log("b")'), 'console.log("a")'); |
| |
| // Navigate forward - should still be filtered |
| assert.strictEqual(history.next(), 'console.log("b")'); |
| }); |
| |
| it('preserves the typed prefix when returning to start', () => { |
| history.pushHistoryItem('console.log("a")'); |
| history.pushHistoryItem('alert("hello")'); |
| |
| // Type "console" and navigate back |
| assert.strictEqual(history.previous('console'), 'console.log("a")'); |
| |
| // Navigate forward - should return to the typed prefix |
| assert.strictEqual(history.next(), 'console'); |
| }); |
| }); |
| |
| }); |
| |
| it('stores the "temporary input" on the first "previous" call with a non-empty history', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| assert.strictEqual(history.previous('incomplete user inp'), 'entry 2'); |
| |
| assert.strictEqual(history.next(''), 'incomplete user inp'); |
| }); |
| |
| it('does not write the temporary user input to the setting', () => { |
| history.pushHistoryItem('entry 1'); |
| assert.strictEqual(history.previous('incomplete user inp'), 'entry 1'); |
| |
| assert.deepEqual(setting.get(), ['entry 1']); |
| }); |
| |
| describe('matchingEntries', () => { |
| it('returns the appropriate matches', () => { |
| history.pushHistoryItem('x === 5'); |
| history.pushHistoryItem('y < 42'); |
| history.pushHistoryItem('x > 20'); |
| |
| const matches = history.matchingEntries('x '); |
| |
| assert.deepEqual([...matches], ['x > 20', 'x === 5']); |
| }); |
| |
| it('respects the "limit" argument', () => { |
| for (let i = 0; i < 20; ++i) { |
| history.pushHistoryItem(`x === ${i}`); |
| } |
| |
| const matches = history.matchingEntries('x ', 3); |
| |
| assert.deepEqual([...matches], ['x === 19', 'x === 18', 'x === 17']); |
| }); |
| }); |
| |
| describe('edit preservation during navigation', () => { |
| it('preserves edits made to history entries when navigating backward', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| history.pushHistoryItem('entry 3'); |
| |
| // Navigate to entry 2 |
| history.previous(''); // now at entry 3 |
| history.previous(''); // now at entry 2 |
| |
| // Edit entry 2 and navigate further back (passing edited text to previous) |
| history.previous('entry 2 EDITED'); // now at entry 1 |
| |
| // Navigate forward - should see the edit |
| assert.strictEqual(history.next(''), 'entry 2 EDITED'); |
| }); |
| |
| it('preserves edits made to history entries when navigating forward', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| history.pushHistoryItem('entry 3'); |
| |
| // Navigate to entry 2 |
| history.previous(''); // now at entry 3 |
| history.previous(''); // now at entry 2 |
| |
| // Edit entry 2 and navigate forward |
| history.next('entry 2 EDITED'); // now at entry 3 |
| |
| // Navigate back - should see the edit |
| assert.strictEqual(history.previous(''), 'entry 2 EDITED'); |
| }); |
| |
| it('preserves multiple edits at different history positions', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| history.pushHistoryItem('entry 3'); |
| |
| // Edit entry 3 by passing edit when navigating away |
| history.previous('entry 3 EDITED'); // save edit, now at entry 2 |
| |
| // Edit entry 2 by passing edit when navigating away |
| history.previous('entry 2 EDITED'); // save edit, now at entry 1 |
| |
| // Navigate forward through edits |
| assert.strictEqual(history.next(''), 'entry 2 EDITED'); |
| assert.strictEqual(history.next(''), 'entry 3 EDITED'); |
| }); |
| |
| it('clears edits when a new command is committed', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| // Edit entry 2 by passing edit when navigating away |
| history.previous('entry 2 EDITED'); |
| |
| // Commit a new command |
| history.pushHistoryItem('entry 3'); |
| |
| // Navigate back - edit should be gone |
| assert.strictEqual(history.previous(''), 'entry 3'); |
| assert.strictEqual(history.previous(''), 'entry 2'); // Original, not edited |
| }); |
| |
| it('preserves uncommitted input alongside history edits', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| // Start typing, then navigate back |
| history.previous('my uncommitted input'); // now at entry 2 |
| |
| // Edit entry 2 and go further back |
| history.previous('entry 2 EDITED'); // now at entry 1 |
| |
| // Navigate all the way forward |
| assert.strictEqual(history.next(''), 'entry 2 EDITED'); |
| assert.strictEqual(history.next(''), 'my uncommitted input'); |
| }); |
| |
| it('restores original if edit matches original', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| // Navigate to entry 2, "edit" it to same value |
| history.previous(''); |
| history.previous('entry 2'); // edit back to original |
| |
| // Should still work |
| assert.strictEqual(history.next(''), 'entry 2'); |
| }); |
| |
| it('restores original if text was emptied', () => { |
| history.pushHistoryItem('entry 1'); |
| history.pushHistoryItem('entry 2'); |
| |
| // Navigate to entry 2, edit it |
| history.previous(''); |
| history.previous('entry 2 EDITED'); // now at entry 1 with edit saved |
| |
| // Verify edit is there |
| assert.strictEqual(history.next(''), 'entry 2 EDITED'); |
| |
| // Now empty the text and navigate away |
| history.previous(''); // empty text, navigate back to entry 1 |
| |
| // Edit should be cleared, original restored |
| assert.strictEqual(history.next(''), 'entry 2'); |
| }); |
| }); |
| |
| }); |