Skip to content

Commit

Permalink
feat(MoveFile): add option to navigate directory to parent
Browse files Browse the repository at this point in the history
  • Loading branch information
sleistner committed Jan 30, 2023
1 parent c9ebb3e commit cdf91bd
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 34 deletions.
18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,26 @@
"fileutils.duplicateFile.typeahead.enabled": {
"type": "boolean",
"default": false,
"description": "Controls whether to show a directory selector for the duplicate file command."
"description": "Controls whether to show a directory selector for the duplicate file command.",
"order": 0
},
"fileutils.moveFile.typeahead.enabled": {
"type": "boolean",
"default": false,
"description": "Controls whether to show a directory selector for the move file command."
"description": "Controls whether to show a directory selector for the move file command.",
"order": 10
},
"fileutils.newFile.typeahead.enabled": {
"type": "boolean",
"default": true,
"description": "Controls whether to show a directory selector for the new file command."
"description": "Controls whether to show a directory selector for the new file command.",
"order": 20
},
"fileutils.newFolder.typeahead.enabled": {
"type": "boolean",
"default": true,
"description": "Controls whether to show a directory selector for new folder command."
"description": "Controls whether to show a directory selector for new folder command.",
"order": 30
},
"fileutils.inputBox.pathType": {
"type": "string",
Expand All @@ -217,14 +221,16 @@
"Absolute file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)",
"Relative file path of the opened workspace or folder (e.g. /myWorkspace)"
],
"description": "Controls the path that is shown in the input box."
"description": "Controls the path that is shown in the input box.",
"order": 40
},
"fileutils.inputBox.pathTypeIndicator": {
"type": "string",
"default": "@",
"maxLength": 50,
"description": "Controls the indicator that is shown in the input box when the path type is workspace. This setting only has an effect when 'fileutils.inputBox.pathType' is set to 'workspace'.",
"markdownDescription": "Controls the indicator that is shown in the input box when the path type is workspace. \n\nThis setting only has an effect when `#fileutils.inputBox.pathType#` is set to `workspace`.\n\nFor example, if the path type is `workspace` and the indicator is `@`, the path will be shown as `@/myWorkspace`."
"markdownDescription": "Controls the indicator that is shown in the input box when the path type is workspace. \n\nThis setting only has an effect when `#fileutils.inputBox.pathType#` is set to `workspace`.\n\nFor example, if the path type is `workspace` and the indicator is `@`, the path will be shown as `@/myWorkspace`.",
"order": 50
},
"fileutils.menus.context.explorer": {
"type": "array",
Expand Down
12 changes: 10 additions & 2 deletions scripts/dev-env
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/usr/bin/env bash

# This script is used to create a development environment for the project.
# It creates a temporary directory with the following structure:


rm -rf ./tmp
mkdir -p ./tmp/{app,workspace,scripts}
touch ./tmp/{app,workspace,scripts}/{foo,bar,baz}.ts
mkdir -p ./tmp/{app,workspace,scripts}/{sub1,sub2,sub3}/{sub-sub1,sub-sub2,sub-sub3}

touch ./tmp/{1..3}.ts
touch ./tmp/{app,workspace,scripts}/file-{1..5}.ts
touch ./tmp/{app,workspace,scripts}/{sub1,sub2,sub3}/sub-file-{1..4}.ts
touch ./tmp/{app,workspace,scripts}/{sub1,sub2,sub3}/{sub-sub1,sub-sub2,sub-sub3}/sub-sub-file-{1..3}.ts
15 changes: 11 additions & 4 deletions src/controller/BaseFileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,21 @@ export abstract class BaseFileController implements FileController {
return workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length > 1;
}

protected async getFileSourcePathAtRoot(rootPath: string, options: SourcePathOptions): Promise<string> {
const { relativeToRoot = false, typeahead } = options;
protected async getFileSourcePathAtRoot(
rootPath: string,
workspaceFolderPath: string,
options: SourcePathOptions
): Promise<string> {
const { relativeToRoot = false, typeahead, showParentFolder } = options;
let sourcePath = rootPath;

if (typeahead) {
const cache = this.getCache(`workspace:${sourcePath}`);
const typeAheadController = new TypeAheadController(cache, relativeToRoot);
sourcePath = await typeAheadController.showDialog(sourcePath);
const typeAheadController = new TypeAheadController(cache, {
relativeToRoot,
showParentFolder,
});
sourcePath = await typeAheadController.showDialog(sourcePath, workspaceFolderPath);
}

if (!sourcePath) {
Expand Down
1 change: 1 addition & 0 deletions src/controller/FileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface SourcePathOptions {
ignoreIfNotExists?: boolean;
uri?: Uri;
typeahead?: boolean;
showParentFolder?: boolean;
}

export interface FileController {
Expand Down
7 changes: 6 additions & 1 deletion src/controller/MoveFileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export class MoveFileController extends BaseFileController {
throw new Error();
}

const rootPath = await this.getFileSourcePathAtRoot(workspaceFolderPath, { relativeToRoot: true, typeahead });
const sourcePathFolder = path.dirname(sourcePath);
const rootPath = await this.getFileSourcePathAtRoot(sourcePathFolder, workspaceFolderPath, {
relativeToRoot: true,
typeahead,
showParentFolder: true,
});
const fileName = path.basename(sourcePath);

return path.join(rootPath, fileName);
Expand Down
2 changes: 1 addition & 1 deletion src/controller/NewFileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class NewFileController extends BaseFileController {
throw new Error();
}

return this.getFileSourcePathAtRoot(rootPath, { relativeToRoot, typeahead });
return this.getFileSourcePathAtRoot(rootPath, rootPath, { relativeToRoot, typeahead });
}

private async getRootPath(relativeToRoot: boolean): Promise<string | undefined> {
Expand Down
131 changes: 111 additions & 20 deletions src/controller/TypeAheadController.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,96 @@
import * as fs from "fs";
import * as path from "path";
import { QuickPickItem, window } from "vscode";
import { QuickPickItem, QuickPickItemKind, window } from "vscode";
import { Cache } from "../lib/Cache";
import { getConfiguration } from "../lib/config";
import { TreeWalker } from "../lib/TreeWalker";

type QuickPickItemHeaderOptions = {
lastEntry: string | undefined;
sourcePath: string;
workspaceFolderPath: string;
};

type TypeAheadControllerOptions = {
relativeToRoot: boolean;
showParentFolder?: boolean;
};

async function waitForIOEvents(): Promise<void> {
return new Promise((resolve) => setImmediate(resolve));
}

const ROOT_PATH = "/";
const WORKSPACE_FOLDER = path.sep;
const CURRENT_FOLDER = ".";
const PARENT_PATH = "..";
const LABEL_PADDING = Math.max(CURRENT_FOLDER.length, WORKSPACE_FOLDER.length, PARENT_PATH.length);

export class TypeAheadController {
constructor(private cache: Cache, private relativeToRoot: boolean = false) {}
private readonly relativeToRoot: boolean;
private readonly showParentFolder: boolean;

public async showDialog(sourcePath: string): Promise<string> {
const items = await this.buildQuickPickItems(sourcePath);
constructor(private cache: Cache, options: TypeAheadControllerOptions) {
this.relativeToRoot = options.relativeToRoot ?? false;
this.showParentFolder = options.showParentFolder ?? false;
}

const item = items.length === 1 ? items[0] : await this.showQuickPick(items);
public async showDialog(sourcePath: string, workspaceFolderPath: string): Promise<string> {
const item = await this.getQuickPickItem(sourcePath, workspaceFolderPath);

if (!item) {
throw new Error();
}

const selection = item.label;
this.cache.put("last", selection);

this.addToCacheIfApplicable(selection);

if (selection === PARENT_PATH) {
return await this.showDialog(path.join(sourcePath, item.label), workspaceFolderPath);
}

if (selection === CURRENT_FOLDER) {
return sourcePath;
}

if (selection === WORKSPACE_FOLDER) {
return workspaceFolderPath;
}

return path.join(sourcePath, selection);
}

private async buildQuickPickItems(sourcePath: string): Promise<QuickPickItem[]> {
private async getQuickPickItem(
sourcePath: string,
workspaceFolderPath: string
): Promise<QuickPickItem | undefined> {
const { header, directories } = await this.buildQuickPickItems(sourcePath, workspaceFolderPath);

if (directories.length === 0 && header.length === 1) {
return header[0];
}

return await this.showQuickPick([...header, ...directories]);
}

private addToCacheIfApplicable(selection: string) {
if (selection !== WORKSPACE_FOLDER && selection !== CURRENT_FOLDER && selection !== PARENT_PATH) {
this.cache.put("last", selection);
}
}

private async buildQuickPickItems(
sourcePath: string,
workspaceFolderPath: string
): Promise<{ header: QuickPickItem[]; directories: QuickPickItem[] }> {
const lastEntry: string = this.cache.get("last");
const header = this.buildQuickPickItemsHeader(lastEntry);
const header = this.buildQuickPickItemsHeader({ lastEntry, sourcePath, workspaceFolderPath });

const directories = (await this.getDirectoriesAtSourcePath(sourcePath))
.filter((directory) => directory !== lastEntry && directory !== ROOT_PATH)
.filter((directory) => directory !== lastEntry && directory !== WORKSPACE_FOLDER)
.map((directory) => this.buildQuickPickItem(directory));

if (directories.length === 0 && header.length === 1) {
return header;
}

return [...header, ...directories];
return { header, directories };
}

private async getDirectoriesAtSourcePath(sourcePath: string): Promise<string[]> {
Expand All @@ -48,20 +99,60 @@ export class TypeAheadController {
return treeWalker.directories(sourcePath);
}

private buildQuickPickItemsHeader(lastEntry: string | undefined): QuickPickItem[] {
private buildQuickPickItemsHeader(options: QuickPickItemHeaderOptions): QuickPickItem[] {
const { lastEntry, sourcePath, workspaceFolderPath } = options;
const items = [];

if (this.relativeToRoot) {
items.push(...this.buildQuickPickItemsHeaderForRelativeRoot(sourcePath, workspaceFolderPath));
} else {
items.push(this.buildQuickPickItem(CURRENT_FOLDER, "current folder"));
}

if (
lastEntry &&
lastEntry !== WORKSPACE_FOLDER &&
lastEntry !== CURRENT_FOLDER &&
fs.existsSync(path.join(sourcePath, lastEntry))
) {
items.push(this.buildQuickPickItem(lastEntry, "last selection"));
}

return items;
}

private buildQuickPickItemsHeaderForRelativeRoot(sourcePath: string, workspaceFolderPath: string): QuickPickItem[] {
const workplaceFolderIndicator: string = getConfiguration("inputBox.pathTypeIndicator") ?? "";

const items = [
this.buildQuickPickItem(ROOT_PATH, `- ${this.relativeToRoot ? "workspace root" : "current file"}`),
{
kind: QuickPickItemKind.Separator,
label: sourcePath.replace(workspaceFolderPath, workplaceFolderIndicator),
},
this.buildQuickPickItem(CURRENT_FOLDER, "current folder"),
];

if (lastEntry && lastEntry !== ROOT_PATH) {
items.push(this.buildQuickPickItem(lastEntry, "- last selection"));
const parentPath = path.join(sourcePath, PARENT_PATH);

if (this.showParentFolder && path.join(workspaceFolderPath, PARENT_PATH) !== parentPath) {
const item = this.buildQuickPickItem(PARENT_PATH, "parent folder");
items.push(item);
}

if (workspaceFolderPath !== sourcePath) {
const item = this.buildQuickPickItem(WORKSPACE_FOLDER, "workspace root");
items.push(item);
}

return items;
}

private buildQuickPickItem(label: string, description?: string | undefined): QuickPickItem {
return { description, label };
if (description) {
const padding = LABEL_PADDING - label.length;
return { label, description: `${"".padStart(padding)}${description}` };
}
return { label };
}

private async showQuickPick(items: readonly QuickPickItem[]) {
Expand Down

0 comments on commit cdf91bd

Please sign in to comment.