Skip to content

Commit 79b6b7e

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
fix: library search boosting
Closes #1106 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 5d264ef commit 79b6b7e

14 files changed

+627
-346
lines changed

Diff for: arduino-ide-extension/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
1818
"watch": "tsc -w",
1919
"test": "mocha \"./lib/test/**/*.test.js\"",
20-
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\"",
20+
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
2121
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
2222
},
2323
"dependencies": {

Diff for: arduino-ide-extension/src/browser/boards/boards-list-widget.ts

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
3030
searchable: service,
3131
installable: service,
3232
itemLabel: (item: BoardsPackage) => item.name,
33-
itemDeprecated: (item: BoardsPackage) => item.deprecated,
3433
itemRenderer,
3534
filterRenderer,
3635
defaultSearchOptions: { query: '', type: 'All' },

Diff for: arduino-ide-extension/src/browser/library/library-list-widget.ts

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export class LibraryListWidget extends ListWidget<
4141
searchable: service,
4242
installable: service,
4343
itemLabel: (item: LibraryPackage) => item.name,
44-
itemDeprecated: (item: LibraryPackage) => item.deprecated,
4544
itemRenderer,
4645
filterRenderer,
4746
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },

Diff for: arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ export namespace ComponentList {
147147
export interface Props<T extends ArduinoComponent> {
148148
readonly items: T[];
149149
readonly itemLabel: (item: T) => string;
150-
readonly itemDeprecated: (item: T) => boolean;
151150
readonly itemRenderer: ListItemRenderer<T>;
152151
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
153152
readonly uninstall: (item: T) => Promise<void>;

Diff for: arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx

+4-9
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,11 @@ export class FilterableListContainer<
8282
}
8383

8484
protected renderComponentList(): React.ReactNode {
85-
const { itemLabel, itemDeprecated, itemRenderer } = this.props;
85+
const { itemLabel, itemRenderer } = this.props;
8686
return (
8787
<ComponentList<T>
8888
items={this.state.items}
8989
itemLabel={itemLabel}
90-
itemDeprecated={itemDeprecated}
9190
itemRenderer={itemRenderer}
9291
install={this.install.bind(this)}
9392
uninstall={this.uninstall.bind(this)}
@@ -109,9 +108,7 @@ export class FilterableListContainer<
109108

110109
protected search(searchOptions: S): void {
111110
const { searchable } = this.props;
112-
searchable
113-
.search(searchOptions)
114-
.then((items) => this.setState({ items: this.props.sort(items) }));
111+
searchable.search(searchOptions).then((items) => this.setState({ items }));
115112
}
116113

117114
protected async install(
@@ -127,7 +124,7 @@ export class FilterableListContainer<
127124
run: ({ progressId }) => install({ item, progressId, version }),
128125
});
129126
const items = await searchable.search(this.state.searchOptions);
130-
this.setState({ items: this.props.sort(items) });
127+
this.setState({ items });
131128
}
132129

133130
protected async uninstall(item: T): Promise<void> {
@@ -155,7 +152,7 @@ export class FilterableListContainer<
155152
run: ({ progressId }) => uninstall({ item, progressId }),
156153
});
157154
const items = await searchable.search(this.state.searchOptions);
158-
this.setState({ items: this.props.sort(items) });
155+
this.setState({ items });
159156
}
160157
}
161158

@@ -168,7 +165,6 @@ export namespace FilterableListContainer {
168165
readonly container: ListWidget<T, S>;
169166
readonly searchable: Searchable<T, S>;
170167
readonly itemLabel: (item: T) => string;
171-
readonly itemDeprecated: (item: T) => boolean;
172168
readonly itemRenderer: ListItemRenderer<T>;
173169
readonly filterRenderer: FilterRenderer<S>;
174170
readonly resolveFocus: (element: HTMLElement | undefined) => void;
@@ -192,7 +188,6 @@ export namespace FilterableListContainer {
192188
progressId: string;
193189
}) => Promise<void>;
194190
readonly commandService: CommandService;
195-
readonly sort: (items: T[]) => T[];
196191
}
197192

198193
export interface State<T, S extends Searchable.Options> {

Diff for: arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx

+1-39
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ export abstract class ListWidget<
5353
*/
5454
protected firstActivate = true;
5555

56-
protected readonly defaultSortComparator: (left: T, right: T) => number;
57-
5856
constructor(protected options: ListWidget.Options<T, S>) {
5957
super();
60-
const { id, label, iconClass, itemDeprecated, itemLabel } = options;
58+
const { id, label, iconClass } = options;
6159
this.id = id;
6260
this.title.label = label;
6361
this.title.caption = label;
@@ -67,15 +65,6 @@ export abstract class ListWidget<
6765
this.node.tabIndex = 0; // To be able to set the focus on the widget.
6866
this.scrollOptions = undefined;
6967
this.toDispose.push(this.searchOptionsChangeEmitter);
70-
71-
this.defaultSortComparator = (left, right): number => {
72-
// always put deprecated items at the bottom of the list
73-
if (itemDeprecated(left)) {
74-
return 1;
75-
}
76-
77-
return itemLabel(left).localeCompare(itemLabel(right));
78-
};
7968
}
8069

8170
@postConstruct()
@@ -144,30 +133,6 @@ export abstract class ListWidget<
144133
return this.options.installable.uninstall({ item, progressId });
145134
}
146135

147-
protected filterableListSort = (items: T[]): T[] => {
148-
const isArduinoTypeComparator = (left: T, right: T) => {
149-
const aIsArduinoType = left.types.includes('Arduino');
150-
const bIsArduinoType = right.types.includes('Arduino');
151-
152-
if (aIsArduinoType && !bIsArduinoType && !left.deprecated) {
153-
return -1;
154-
}
155-
156-
if (!aIsArduinoType && bIsArduinoType && !right.deprecated) {
157-
return 1;
158-
}
159-
160-
return 0;
161-
};
162-
163-
return items.sort((left, right) => {
164-
return (
165-
isArduinoTypeComparator(left, right) ||
166-
this.defaultSortComparator(left, right)
167-
);
168-
});
169-
};
170-
171136
render(): React.ReactNode {
172137
return (
173138
<FilterableListContainer<T, S>
@@ -178,14 +143,12 @@ export abstract class ListWidget<
178143
install={this.install.bind(this)}
179144
uninstall={this.uninstall.bind(this)}
180145
itemLabel={this.options.itemLabel}
181-
itemDeprecated={this.options.itemDeprecated}
182146
itemRenderer={this.options.itemRenderer}
183147
filterRenderer={this.options.filterRenderer}
184148
searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
185149
messageService={this.messageService}
186150
commandService={this.commandService}
187151
responseService={this.responseService}
188-
sort={this.filterableListSort}
189152
/>
190153
);
191154
}
@@ -218,7 +181,6 @@ export namespace ListWidget {
218181
readonly installable: Installable<T>;
219182
readonly searchable: Searchable<T, S>;
220183
readonly itemLabel: (item: T) => string;
221-
readonly itemDeprecated: (item: T) => boolean;
222184
readonly itemRenderer: ListItemRenderer<T>;
223185
readonly filterRenderer: FilterRenderer<S>;
224186
readonly defaultSearchOptions: S;

Diff for: arduino-ide-extension/src/common/protocol/arduino-component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Installable } from './installable';
22

33
export interface ArduinoComponent {
44
readonly name: string;
5-
readonly deprecated: boolean;
5+
readonly deprecated?: boolean;
66
readonly author: string;
77
readonly summary: string;
88
readonly description: string;

Diff for: arduino-ide-extension/src/common/protocol/searchable.ts

+29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import URI from '@theia/core/lib/common/uri';
2+
import type { ArduinoComponent } from './arduino-component';
23

34
export interface Searchable<T, O extends Searchable.Options> {
45
search(options: O): Promise<T[]>;
@@ -31,3 +32,31 @@ export namespace Searchable {
3132
}
3233
}
3334
}
35+
36+
// IDE2 must keep the library search order from the CLI but do additional boosting
37+
// https://github.com/arduino/arduino-ide/issues/1106
38+
// This additional search result boosting considers the following groups: 'Arduino', '', 'Arduino-Retired', and 'Retired'.
39+
// If two libraries fall into the same group, the original index is the tiebreaker.
40+
export type SortGroup = 'Arduino' | '' | 'Arduino-Retired' | 'Retired';
41+
const sortGroupOrder: Record<SortGroup, number> = {
42+
Arduino: 0,
43+
'': 1,
44+
'Arduino-Retired': 2,
45+
Retired: 3,
46+
};
47+
48+
export function sortComponents<T extends ArduinoComponent>(
49+
components: T[],
50+
group: (component: T) => SortGroup
51+
): T[] {
52+
return components
53+
.map((component, index) => ({ ...component, index }))
54+
.sort((left, right) => {
55+
const leftGroup = group(left);
56+
const rightGroup = group(right);
57+
if (leftGroup === rightGroup) {
58+
return left.index - right.index;
59+
}
60+
return sortGroupOrder[leftGroup] - sortGroupOrder[rightGroup];
61+
});
62+
}

Diff for: arduino-ide-extension/src/node/boards-service-impl.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
BoardWithPackage,
1818
BoardUserField,
1919
BoardSearch,
20+
sortComponents,
21+
SortGroup,
2022
} from '../common/protocol';
2123
import {
2224
PlatformInstallRequest,
@@ -405,7 +407,8 @@ export class BoardsServiceImpl
405407
}
406408

407409
const filter = this.typePredicate(options);
408-
return [...packages.values()].filter(filter);
410+
const boardsPackages = [...packages.values()].filter(filter);
411+
return sortComponents(boardsPackages, boardsPackageSortGroup);
409412
}
410413

411414
private typePredicate(
@@ -559,3 +562,14 @@ function isMissingPlatformError(error: unknown): boolean {
559562
}
560563
return false;
561564
}
565+
566+
function boardsPackageSortGroup(boardsPackage: BoardsPackage): SortGroup {
567+
const types: string[] = [];
568+
if (boardsPackage.types.includes('Arduino')) {
569+
types.push('Arduino');
570+
}
571+
if (boardsPackage.deprecated) {
572+
types.push('Retired');
573+
}
574+
return types.join('-') as SortGroup;
575+
}

Diff for: arduino-ide-extension/src/node/library-service-impl.ts

+30-12
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
import { injectable, inject } from '@theia/core/shared/inversify';
1+
import { ILogger, notEmpty } from '@theia/core';
2+
import { FileUri } from '@theia/core/lib/node';
3+
import { inject, injectable } from '@theia/core/shared/inversify';
4+
import { duration } from '../common/decorators';
5+
import {
6+
NotificationServiceServer,
7+
ResponseService,
8+
sortComponents,
9+
SortGroup,
10+
} from '../common/protocol';
11+
import { Installable } from '../common/protocol/installable';
212
import {
313
LibraryDependency,
414
LibraryLocation,
515
LibraryPackage,
616
LibrarySearch,
717
LibraryService,
818
} from '../common/protocol/library-service';
9-
import { CoreClientAware } from './core-client-provider';
1019
import { BoardDiscovery } from './board-discovery';
1120
import {
1221
InstalledLibrary,
1322
Library,
23+
LibraryInstallLocation,
1424
LibraryInstallRequest,
1525
LibraryListRequest,
1626
LibraryListResponse,
1727
LibraryLocation as GrpcLibraryLocation,
1828
LibraryRelease,
1929
LibraryResolveDependenciesRequest,
20-
LibraryUninstallRequest,
21-
ZipLibraryInstallRequest,
2230
LibrarySearchRequest,
2331
LibrarySearchResponse,
24-
LibraryInstallLocation,
32+
LibraryUninstallRequest,
33+
ZipLibraryInstallRequest,
2534
} from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb';
26-
import { Installable } from '../common/protocol/installable';
27-
import { ILogger, notEmpty } from '@theia/core';
28-
import { FileUri } from '@theia/core/lib/node';
29-
import { ResponseService, NotificationServiceServer } from '../common/protocol';
35+
import { CoreClientAware } from './core-client-provider';
3036
import { ExecuteWithProgress } from './grpc-progressible';
31-
import { duration } from '../common/decorators';
3237

3338
@injectable()
3439
export class LibraryServiceImpl
@@ -108,7 +113,10 @@ export class LibraryServiceImpl
108113

109114
const typePredicate = this.typePredicate(options);
110115
const topicPredicate = this.topicPredicate(options);
111-
return items.filter((item) => typePredicate(item) && topicPredicate(item));
116+
const libraries = items.filter(
117+
(item) => typePredicate(item) && topicPredicate(item)
118+
);
119+
return sortComponents(libraries, librarySortGroup);
112120
}
113121

114122
private typePredicate(
@@ -448,7 +456,6 @@ function toLibrary(
448456
name: '',
449457
exampleUris: [],
450458
installable: false,
451-
deprecated: false,
452459
location: 0,
453460
...pkg,
454461

@@ -462,3 +469,14 @@ function toLibrary(
462469
types: lib.getTypesList(),
463470
};
464471
}
472+
473+
// Libraries do not have a deprecated property. The deprecated information is inferred if 'Retired' is in 'types'
474+
function librarySortGroup(library: LibraryPackage): SortGroup {
475+
const types: string[] = [];
476+
for (const type of ['Arduino', 'Retired']) {
477+
if (library.types.includes(type)) {
478+
types.push(type);
479+
}
480+
}
481+
return types.join('-') as SortGroup;
482+
}

0 commit comments

Comments
 (0)