From e07e4d6b42ea83f480fac03f4b0dba288fd777ca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:01:57 -0800 Subject: [PATCH] Add tests for simple completion model and fix bad sorting issue --- .../suggest/browser/simpleCompletionModel.ts | 14 +- .../browser/simpleCompletionModel.test.ts | 198 ++++++++++++++++++ 2 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 4031bba7bae30f..eff68db51be15c 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -195,9 +195,14 @@ export class SimpleCompletionModel { return score; } - // Sort files with the same score against each other specially + // Sort by underscore penalty (eg. `__init__/` should be penalized) + if (a.underscorePenalty !== b.underscorePenalty) { + return a.underscorePenalty - b.underscorePenalty; + } + + // Sort files of the same name by extension const isArg = leadingLineContent.includes(' '); - if (!isArg && a.fileExtLow.length > 0 && b.fileExtLow.length > 0) { + if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) { // Then by label length ascending (excluding file extension if it's a file) score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length; if (score !== 0) { @@ -215,11 +220,6 @@ export class SimpleCompletionModel { } } - // Sort by underscore penalty (eg. `__init__/` should be penalized) - if (a.underscorePenalty !== b.underscorePenalty) { - return a.underscorePenalty - b.underscorePenalty; - } - // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { // Directories diff --git a/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts new file mode 100644 index 00000000000000..d846b8fa5accb0 --- /dev/null +++ b/src/vs/workbench/services/suggest/test/browser/simpleCompletionModel.test.ts @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { LineContext, SimpleCompletionModel } from '../../browser/simpleCompletionModel.js'; +import { SimpleCompletionItem, type ISimpleCompletion } from '../../browser/simpleCompletionItem.js'; + +function createItem(options: Partial): SimpleCompletionItem { + return new SimpleCompletionItem({ + ...options, + label: options.label || 'defaultLabel', + provider: options.provider || 'defaultProvider', + replacementIndex: options.replacementIndex || 0, + replacementLength: options.replacementLength || 1, + }); +} + +function createFileItems(...labels: string[]): SimpleCompletionItem[] { + return labels.map(label => createItem({ label, isFile: true })); +} + +function createFileItemsModel(...labels: string[]): SimpleCompletionModel { + return new SimpleCompletionModel( + createFileItems(...labels), + new LineContext('', 0) + ); +} + +function createFolderItems(...labels: string[]): SimpleCompletionItem[] { + return labels.map(label => createItem({ label, isDirectory: true })); +} + +// function createFolderItemsModel(...labels: string[]): SimpleCompletionModel { +// return new SimpleCompletionModel( +// createFolderItems(...labels), +// new LineContext('', 0) +// ); +// } + +function assertItems(model: SimpleCompletionModel, labels: string[]): void { + assert.deepStrictEqual(model.items.map(i => i.completion.label), labels); + assert.strictEqual(model.items.length, labels.length); // sanity check +} + +suite('SimpleCompletionModel', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + let model: SimpleCompletionModel; + + test('should handle an empty list', function () { + model = new SimpleCompletionModel([], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 0); + }); + + test('should handle a list with one item', function () { + model = new SimpleCompletionModel([ + createItem({ label: 'a' }), + ], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 1); + assert.strictEqual(model.items[0].completion.label, 'a'); + }); + + test('should sort alphabetically', function () { + model = new SimpleCompletionModel([ + createItem({ label: 'b' }), + createItem({ label: 'z' }), + createItem({ label: 'a' }), + ], new LineContext('', 0)); + + assert.strictEqual(model.items.length, 3); + assert.strictEqual(model.items[0].completion.label, 'a'); + assert.strictEqual(model.items[1].completion.label, 'b'); + assert.strictEqual(model.items[2].completion.label, 'z'); + }); + + suite('files', () => { + test('should deprioritize files that start with underscore', function () { + assertItems(createFileItemsModel('_a', 'a', 'z'), ['a', 'z', '_a']); + }); + + test('should ignore the dot in dotfiles when sorting', function () { + assertItems(createFileItemsModel('b', '.a', 'a', '.b'), ['.a', 'a', 'b', '.b']); + }); + }); + + suite('folders', () => { + }); + + suite('files and folders', () => { + test('should handle many files and folders correctly', function () { + // This is VS Code's root directory with some python items added that have special + // sorting + const items = [ + ...createFolderItems( + '__pycache', + '.build', + '.configurations', + '.devcontainer', + '.eslint-plugin-local', + '.github', + '.profile-oss', + '.vscode', + '.vscode-test', + 'build', + 'cli', + 'extensions', + 'node_modules', + 'out', + 'remote', + 'resources', + 'scripts', + 'src', + 'test', + ), + ...createFileItems( + '__init__.py', + '.editorconfig', + '.eslint-ignore', + '.git-blame-ignore-revs', + '.gitattributes', + '.gitignore', + '.lsifrc.json', + '.mailmap', + '.mention-bot', + '.npmrc', + '.nvmrc', + '.vscode-test.js', + 'cglicenses.json', + 'cgmanifest.json', + 'CodeQL.yml', + 'CONTRIBUTING.md', + 'eslint.config.js', + 'gulpfile.js', + 'LICENSE.txt', + 'package-lock.json', + 'package.json', + 'product.json', + 'README.md', + 'SECURITY.md', + 'ThirdPartyNotices.txt', + 'tsfmt.json', + ) + ]; + const model = new SimpleCompletionModel(items, new LineContext('', 0)); + assertItems(model, [ + '.build', + 'build', + 'cglicenses.json', + 'cgmanifest.json', + 'cli', + 'CodeQL.yml', + '.configurations', + 'CONTRIBUTING.md', + '.devcontainer', + '.npmrc', + '.gitignore', + '.editorconfig', + 'eslint.config.js', + '.eslint-ignore', + '.eslint-plugin-local', + 'extensions', + '.gitattributes', + '.git-blame-ignore-revs', + '.github', + 'gulpfile.js', + 'LICENSE.txt', + '.lsifrc.json', + '.nvmrc', + '.mailmap', + '.mention-bot', + 'node_modules', + 'out', + 'package.json', + 'package-lock.json', + 'product.json', + '.profile-oss', + 'README.md', + 'remote', + 'resources', + 'scripts', + 'SECURITY.md', + 'src', + 'test', + 'ThirdPartyNotices.txt', + 'tsfmt.json', + '.vscode', + '.vscode-test', + '.vscode-test.js', + '__init__.py', + '__pycache', + ]); + }); + }); +});