feature/vs code filter tree view (#2561)

* - adds a welcome view for the vscode explorer

* - moves the nodes loading to a separate method

* - implements get parent function

* - temp filtering with ugly copy

* - much better filtering implementation with big o one and zero allocs

* - code linting

* - implements filter box

* - removes clear filter button

* - remembers the filter between prompts

* - adds french translations for the filtering capability
This commit is contained in:
Vincent Biret 2023-04-14 03:45:25 -04:00 коммит произвёл GitHub
Родитель 2bbc155bff
Коммит 22a80d658a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 212 добавлений и 86 удалений

Просмотреть файл

@ -26,5 +26,9 @@
"Downloading kiota...": "Téléchargement de Kiota...",
"Generating client...": "Génération du client...",
"Updating clients...": "Mise à jour des clients...",
"Loading...": "Chargement..."
"Loading...": "Chargement...",
"Pick a lock file": "Sélectionnez un fichier verrou",
"Open a lock file": "Ouvrir un fichier verrou",
"Filter the API description": "Filtrer la description d'API",
"Enter a filter": "Entrez un filtre"
}

Просмотреть файл

@ -21,6 +21,12 @@
"main": "./dist/extension.js",
"l10n": "./l10n",
"contributes": {
"viewsWelcome":[
{
"view": "kiota.openApiExplorer",
"contents": "%kiota.openApiExplorer.welcome%"
}
],
"viewsContainers": {
"activitybar": [
{
@ -77,14 +83,19 @@
"group": "navigation@1"
},
{
"command": "kiota.openApiExplorer.generateClient",
"command": "kiota.openApiExplorer.filterDescription",
"when": "view == kiota.openApiExplorer",
"group": "navigation@3"
},
{
"command": "kiota.openApiExplorer.closeDescription",
"command": "kiota.openApiExplorer.generateClient",
"when": "view == kiota.openApiExplorer",
"group": "navigation@4"
},
{
"command": "kiota.openApiExplorer.closeDescription",
"when": "view == kiota.openApiExplorer",
"group": "navigation@5"
}
],
"view/item/context": [
@ -139,6 +150,12 @@
"title": "%kiota.selectLock.title%",
"icon": "$(file-symlink-file)"
},
{
"command": "kiota.searchLock",
"category": "Kiota",
"title": "%kiota.searchLock.title%",
"icon": "$(file-symlink-file)"
},
{
"command": "kiota.updateClients",
"category": "Kiota",
@ -150,6 +167,12 @@
"title": "%kiota.openApiExplorer.generateClient.title%",
"icon": "$(play)"
},
{
"command": "kiota.openApiExplorer.filterDescription",
"category": "Kiota",
"title": "%kiota.openApiExplorer.filterDescription.title%",
"icon": "$(filter)"
},
{
"command": "kiota.searchApiDescription",
"category": "Kiota",

Просмотреть файл

@ -14,5 +14,8 @@
"kiota.openApiExplorer.addAllToSelectedEndpoints.title": "Tout ajouter",
"kiota.openApiExplorer.removeAllFromSelectedEndpoints.title": "Tout supprimer",
"kiota.openApiExplorer.closeDescription.title": "Fermer la description d'API",
"kiota.openApiExplorer.openDescription.title": "Ouvrir une description d'API"
"kiota.openApiExplorer.openDescription.title": "Ouvrir une description d'API",
"kiota.openApiExplorer.welcome": "Aucune description sélectionnée.\n[Rechercher](command:kiota.searchApiDescription)\n[Ouvrir](command:kiota.openApiExplorer.openDescription)\n[Sélectionner un fichier verrou](command:kiota.searchLock)",
"kiota.searchLock.title": "Rechercher un fichier verrou",
"kiota.openApiExplorer.filterDescription.title": "Filtrer la description"
}

Просмотреть файл

@ -14,5 +14,8 @@
"kiota.openApiExplorer.addAllToSelectedEndpoints.title": "Add all",
"kiota.openApiExplorer.removeAllFromSelectedEndpoints.title": "Remove all",
"kiota.openApiExplorer.closeDescription.title": "Close API description",
"kiota.openApiExplorer.openDescription.title": "Open API description"
"kiota.openApiExplorer.openDescription.title": "Open API description",
"kiota.openApiExplorer.welcome": "No API description selected.\n[Search](command:kiota.searchApiDescription)\n[Open](command:kiota.openApiExplorer.openDescription)\n[Select lock file](command:kiota.searchLock)",
"kiota.searchLock.title": "Search for a lock file",
"kiota.openApiExplorer.filterDescription.title": "Filter API description"
}

Просмотреть файл

@ -10,7 +10,7 @@ import {
LogLevel,
parseGenerationLanguage,
} from "./kiotaInterop";
import { generateSteps, openSteps, searchSteps } from "./steps";
import { filterSteps, generateSteps, openSteps, searchLockSteps, searchSteps } from "./steps";
import { getKiotaVersion } from "./getKiotaVersion";
import { searchDescription } from "./searchDescription";
import { generateClient } from "./generateClient";
@ -20,6 +20,12 @@ import { updateClients } from "./updateClients";
let kiotaStatusBarItem: vscode.StatusBarItem;
let kiotaOutputChannel: vscode.LogOutputChannel;
const extensionId = "kiota";
const focusCommandId = ".focus";
const statusBarCommandId = `${extensionId}.status`;
const treeViewId = `${extensionId}.openApiExplorer`;
const dependenciesInfo = `${extensionId}.dependenciesInfo`;
export const kiotaLockFile = "kiota-lock.json";
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
@ -29,28 +35,22 @@ export async function activate(
kiotaOutputChannel = vscode.window.createOutputChannel("Kiota", {
log: true,
});
const extensionId = "kiota";
const focusCommandId = ".focus";
const statusBarCommandId = `${extensionId}.status`;
const treeViewId = `${extensionId}.openApiExplorer`;
const dependenciesInfo = `${extensionId}.dependenciesInfo`;
const openApiTreeProvider = new OpenApiTreeProvider(context);
const dependenciesInfoProvider = new DependenciesViewProvider(
context.extensionUri
);
context.subscriptions.push(
vscode.commands.registerCommand(
`${extensionId}.selectLock`,
async (node: { fsPath: string }) => {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
cancellable: false,
title: vscode.l10n.t("Loading...")
}, (progress, _) => openApiTreeProvider.loadLockFile(node.fsPath));
if (openApiTreeProvider.descriptionUrl) {
await vscode.commands.executeCommand(`${treeViewId}${focusCommandId}`);
`${extensionId}.searchLock`,
async () => {
const lockFilePath = await searchLockSteps();
if (lockFilePath && lockFilePath.lockFilePath) {
await loadLockFile(lockFilePath.lockFilePath, openApiTreeProvider);
}
}
}),
vscode.commands.registerCommand(
`${extensionId}.selectLock`,
(x) => loadLockFile(x, openApiTreeProvider)
),
vscode.commands.registerCommand(statusBarCommandId, async () => {
const yesAnswer = vscode.l10n.t("Yes");
@ -158,7 +158,7 @@ export async function activate(
if (typeof config.outputPath === "string" && !openApiTreeProvider.isLockFileLoaded &&
vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 &&
result && getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length === 0) {
await openApiTreeProvider.loadLockFile(path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, config.outputPath, "kiota-lock.json"));
await openApiTreeProvider.loadLockFile(path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, config.outputPath, kiotaLockFile));
}
if (result)
{
@ -171,7 +171,7 @@ export async function activate(
async () => {
const config = await searchSteps(x => searchDescription(context, x));
if (config.descriptionPath) {
openApiTreeProvider.descriptionUrl = config.descriptionPath;
await openApiTreeProvider.setDescriptionUrl(config.descriptionPath);
await vscode.commands.executeCommand(`${treeViewId}${focusCommandId}`);
}
}
@ -179,12 +179,17 @@ export async function activate(
vscode.commands.registerCommand(`${treeViewId}.closeDescription`, () =>
openApiTreeProvider.closeDescription()
),
vscode.commands.registerCommand(`${treeViewId}.filterDescription`,
async () => {
await filterSteps(openApiTreeProvider.filter, x => openApiTreeProvider.filter = x);
}
),
vscode.commands.registerCommand(
`${treeViewId}.openDescription`,
async () => {
const openState = await openSteps();
if (openState.descriptionPath) {
openApiTreeProvider.descriptionUrl = openState.descriptionPath;
await openApiTreeProvider.setDescriptionUrl(openState.descriptionPath);
await vscode.commands.executeCommand(`${treeViewId}${focusCommandId}`);
}
}
@ -248,6 +253,17 @@ export async function activate(
context.subscriptions.push(disposable);
}
async function loadLockFile(node: { fsPath: string }, openApiTreeProvider: OpenApiTreeProvider): Promise<void> {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
cancellable: false,
title: vscode.l10n.t("Loading...")
}, (progress, _) => openApiTreeProvider.loadLockFile(node.fsPath));
if (openApiTreeProvider.descriptionUrl) {
await vscode.commands.executeCommand(`${treeViewId}${focusCommandId}`);
}
}
async function exportLogsAndShowErrors(result: KiotaLogEntry[]) : Promise<void> {
const informationMessages = result
? getLogEntriesForLevel(result, LogLevel.information)

Просмотреть файл

@ -25,7 +25,7 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
this._lockFile = JSON.parse(lockFileData.toString()) as LockFile;
if (this._lockFile?.descriptionLocation) {
this._descriptionUrl = this._lockFile.descriptionLocation;
await this.getChildren();
await this.loadNodes();
if (this.rawRootNode) {
if (this._lockFile.includePatterns.length === 0) {
this.setAllSelected(this.rawRootNode, true);
@ -37,7 +37,7 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
}
});
}
this.refresh();
this.refreshView();
}
}
}
@ -62,14 +62,17 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
this.rawRootNode = undefined;
this._lockFile = undefined;
this._lockFilePath = undefined;
this.tokenizedFilter = [];
this._filterText = '';
if (shouldRefresh) {
this.refresh();
this.refreshView();
}
}
public set descriptionUrl(descriptionUrl: string) {
public async setDescriptionUrl(descriptionUrl: string): Promise<void> {
this.closeDescription(false);
this._descriptionUrl = descriptionUrl;
this.refresh();
await this.loadNodes();
this.refreshView();
}
public get descriptionUrl(): string {
return this._descriptionUrl || '';
@ -81,7 +84,7 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
const apiNode = this.findApiNode(this.getPathSegments(item.path), this.rawRootNode);
if(apiNode) {
this.selectInternal(apiNode, selected, recursive);
this.refresh();
this.refreshView();
}
}
private selectInternal(apiNode: KiotaOpenApiNode, selected: boolean, recursive: boolean) {
@ -104,7 +107,7 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
return undefined;
}
refresh(): void {
refreshView(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: OpenApiTreeNode): vscode.TreeItem {
@ -127,75 +130,94 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider<OpenApiTreeN
private getPathSegments(path: string): string[] {
return path.replace('/', '').split('\\').filter(x => x !== ''); // the root node is always /
}
private readonly selectedSet: IconSet = new vscode.ThemeIcon('check');
private readonly unselectedSet: IconSet = new vscode.ThemeIcon('circle-slash');
private getIconSet(selected: boolean): IconSet {
return selected ? this.selectedSet : this.unselectedSet;
}
private rawRootNode: KiotaOpenApiNode | undefined;
async getChildren(element?: OpenApiTreeNode): Promise<OpenApiTreeNode[]> {
private tokenizedFilter: string[] = [];
private _filterText: string = '';
public set filter(filterText: string) {
this._filterText = filterText;
if (!this.rawRootNode) {
return;
}
this.tokenizedFilter = filterText.length === 0 ? [] : filterText.split(' ').filter(x => x !== '').map(x => x.trim().toLowerCase());
this.refreshView();
}
public get filter(): string {
return this._filterText;
}
private async loadNodes(): Promise<void> {
if (!this.descriptionUrl || this.descriptionUrl.length === 0) {
return;
}
const result = await connectToKiota(this.context, async (connection) => {
const request = new rpc.RequestType<KiotaShowConfiguration, KiotaShowResult, void>('Show');
return await connection.sendRequest(request, {
includeFilters: this.includeFilters,
excludeFilters: this.excludeFilters,
descriptionPath: this.descriptionUrl
});
});
if(result && result.rootNode) {
this.rawRootNode = result.rootNode;
}
}
getCollapsedState(hasChildren: boolean): vscode.TreeItemCollapsibleState {
return !hasChildren ?
vscode.TreeItemCollapsibleState.None :
(this.tokenizedFilter.length === 0 ?
vscode.TreeItemCollapsibleState.Collapsed :
vscode.TreeItemCollapsibleState.Expanded);
}
getTreeNodeFromKiotaNode(node: KiotaOpenApiNode, parent?: OpenApiTreeNode): OpenApiTreeNode {
const result = new OpenApiTreeNode(
node.path,
node.segment,
node.selected,
this.getCollapsedState(node.children.length > 0)
);
result.children = node.children.map(x => this.getTreeNodeFromKiotaNode(x, result));
return result;
}
getChildren(element?: OpenApiTreeNode): OpenApiTreeNode[] {
if (!this.rawRootNode) {
return [];
}
if (!this.rawRootNode) {
const result = await connectToKiota(this.context, async (connection) => {
const request = new rpc.RequestType<KiotaShowConfiguration, KiotaShowResult, void>('Show');
return await connection.sendRequest(request, {
includeFilters: this.includeFilters,
excludeFilters: this.excludeFilters,
descriptionPath: this.descriptionUrl
});
});
if(result && result.rootNode) {
this.rawRootNode = result.rootNode;
}
else {
return [];
}
}
if (element) {
return this.findChildren(this.getPathSegments(element.path), this.rawRootNode)
.map(x => new OpenApiTreeNode(x.path,
x.segment,
x.selected,
this.getIconSet(x.selected),
x.children.length > 0 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
));
return element.children.filter(x => x.isNodeVisible(this.tokenizedFilter));
} else {
return [new OpenApiTreeNode(this.rawRootNode.path,
this.rawRootNode.segment,
this.rawRootNode.selected,
this.getIconSet(this.rawRootNode.selected),
vscode.TreeItemCollapsibleState.Expanded)];
const result = this.getTreeNodeFromKiotaNode(this.rawRootNode);
result.collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
return [result];
}
}
private findChildren(segments: string[], currentNode: KiotaOpenApiNode): KiotaOpenApiNode[] {
if(segments.length === 0) {
return currentNode.children;
} else {
const segment = segments.shift();
if(segment) {
const child = currentNode.children.find(x => x.segment === segment);
if(child) {
return this.findChildren(segments, child);
}
}
}
return [];
}
}
type IconSet = string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri } | vscode.ThemeIcon;
export class OpenApiTreeNode extends vscode.TreeItem {
private static readonly selectedSet: IconSet = new vscode.ThemeIcon('check');
private static readonly unselectedSet: IconSet = new vscode.ThemeIcon('circle-slash');
public children: OpenApiTreeNode[];
constructor(
public readonly path: string,
public readonly label: string,
public selected: boolean,
iconSet: IconSet,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly command?: vscode.Command
public readonly selected: boolean,
public collapsibleState: vscode.TreeItemCollapsibleState,
_children?: OpenApiTreeNode[]
) {
super(label, collapsibleState);
this.iconPath = iconSet;
this.iconPath = selected ? OpenApiTreeNode.selectedSet : OpenApiTreeNode.unselectedSet;
if (_children) {
this.children = _children;
} else {
this.children = [];
}
}
public isNodeVisible(tokenizedFilter: string[]): boolean {
if (tokenizedFilter.length === 0) {
return true;
}
if (this.children.length === 0) {
const lowerCaseSegment = this.label.toLowerCase();
return tokenizedFilter.some(x => lowerCaseSegment.includes(x.toLowerCase()));
}
return this.children.some(x => x.isNodeVisible(tokenizedFilter));
}
}

Просмотреть файл

@ -1,5 +1,6 @@
import { QuickPickItem, window, Disposable, QuickInputButton, QuickInput, QuickInputButtons, workspace, l10n } from 'vscode';
import { QuickPickItem, window, Disposable, QuickInputButton, QuickInput, QuickInputButtons, workspace, l10n, Uri } from 'vscode';
import { allGenerationLanguages, generationLanguageToString, KiotaSearchResultItem, LanguagesInformation, maturityLevelToString } from './kiotaInterop';
import { kiotaLockFile } from './extension';
export async function openSteps() {
@ -22,6 +23,57 @@ export async function openSteps() {
return state;
};
export async function searchLockSteps() {
const state = {} as Partial<SearchLockState>;
let step = 1;
let totalSteps = 1;
const title = l10n.t('Open a lock file');
async function pickSearchResult(input: MultiStepInput, state: Partial<SearchLockState>) {
const searchResults = await workspace.findFiles(`**/${kiotaLockFile}`);
const items = searchResults.map(x =>
{
return {
label: x.path.replace(workspace.getWorkspaceFolder(x)?.uri.path || '', ''),
} as QuickPickItem;
});
const pick = await input.showQuickPick({
title,
step: step++,
totalSteps: totalSteps,
placeholder: l10n.t('Pick a lock file'),
items: items,
shouldResume: shouldResume
});
state.lockFilePath = searchResults.find(x => x.path.replace(workspace.getWorkspaceFolder(x)?.uri.path || '', '') === pick?.label);
}
await MultiStepInput.run(input => pickSearchResult(input, state), () => step-=2);
return state;
}
export async function filterSteps(existingFilter: string, filterCallback: (searchQuery: string) => void) {
const state = {} as Partial<BaseStepsState>;
const title = l10n.t('Filter the API description');
let step = 1;
let totalSteps = 1;
async function inputFilterQuery(input: MultiStepInput, state: Partial<BaseStepsState>) {
await input.showInputBox({
title,
step: step++,
totalSteps: totalSteps,
value: existingFilter,
prompt: l10n.t('Enter a filter'),
validate: x => {
filterCallback(x.length === 0 && existingFilter.length > 0 ? existingFilter : x);
existingFilter = '';
return Promise.resolve(undefined);
},
shouldResume: shouldResume
});
}
await MultiStepInput.run(input => inputFilterQuery(input, state), () => step-=2);
return state;
}
export async function searchSteps(searchCallBack: (searchQuery: string) => Promise<Record<string, KiotaSearchResultItem> | undefined>) {
const state = {} as Partial<SearchState>;
const title = l10n.t('Search for an API description');
@ -173,6 +225,9 @@ interface OpenState extends BaseStepsState {
descriptionPath: string;
}
interface SearchLockState extends BaseStepsState {
lockFilePath: Uri;
}
interface GenerateState extends BaseStepsState {
clientClassName: string;