Repository: oliverschwendener/ueli (ts)

Issue:
[Feature Request] Visual Studio Code extension search file path also
**Is your feature request related to a problem? Please describe.**

Currently vscode extension seems only search vscode recent history, but not actual file path.

eg. I have a folder `~/script`, and doesn't matter partial or full path, the search window will show nothing.

**Describe the solution you'd like**

vscode extension can search both vscode recent and actual path.

**Describe alternatives you've considered**

n/a

**Additional context**

Nothing show up with partial path:
![Image](https://github.com/user-attachments/assets/1cf1989d-a5a1-415c-b0c5-7ce3743bcb7e)

Still nothing with full path:
![Image](https://github.com/user-attachments/assets/6564a350-bc5d-4417-aaf5-e889593bf092)

Pull request description:
Add search filesystem to vscode extension
This PR add the ability to open folder and files from Ueli in vscode other than just the recently opened stuff.

closes #1338

![image](https://github.com/user-attachments/assets/c3336248-7190-4694-abf4-e5aff45f3ebe)


Patch:
diff --git a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
index 50005e1d..43b2d3f5 100644
--- a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
@@ -9,6 +9,7 @@ export interface FileSystemUtility {
     readJsonFile<T>(filePath: string): Promise<T>;
     readJsonFileSync<T>(filePath: string): T;
     readDirectory(folderPath: string, recursive?: boolean): Promise<string[]>;
+    readDirectorySync(folderPath: string, recursive?: boolean): string[];
     removeFile(filePath: string): Promise<void>;
     removeFileSync(filePath: string): void;
     writeTextFile(data: string, filePath: string): Promise<void>;
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
index b725e125..322f1ced 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
@@ -1,4 +1,4 @@
-import { access, accessSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
+import { access, accessSync, existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
 import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
 import { join } from "path";
 import type { FileSystemUtility } from "./Contract";
@@ -100,4 +100,9 @@ export class NodeJsFileSystemUtility implements FileSystemUtility {
         const fileNames = await readdir(folderPath, { recursive });
         return fileNames.map((fileName) => join(folderPath, fileName));
     }
+
+    public readDirectorySync(folderPath: string, recursive?: boolean): string[] {
+        const fileNames = readdirSync(folderPath, { recursive });
+        return fileNames.map((fileName) => join(folderPath, fileName.toString()));
+    }
 }
diff --git a/src/main/Extensions/VSCode/VSCodeExtension.ts b/src/main/Extensions/VSCode/VSCodeExtension.ts
index 234a16e3..204fb6d4 100644
--- a/src/main/Extensions/VSCode/VSCodeExtension.ts
+++ b/src/main/Extensions/VSCode/VSCodeExtension.ts
@@ -1,5 +1,6 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { Extension } from "@Core/Extension";
+import type { FileSystemUtility } from "@Core/FileSystemUtility";
 import type { FileImageGenerator } from "@Core/ImageGenerator";
 import type { Logger } from "@Core/Logger";
 import type { SettingsManager } from "@Core/SettingsManager";
@@ -64,6 +65,7 @@ export class VSCodeExtension implements Extension {
         private readonly logger: Logger,
         private readonly settingsManager: SettingsManager,
         private readonly fileImageGenerator: FileImageGenerator,
+        private readonly fileSystemUtility: FileSystemUtility,
     ) {}
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
@@ -235,10 +237,18 @@ export class VSCodeExtension implements Extension {
             this.getSettingDefaultValue("showPath"),
         );
 
-        const searchResultItems = this.recents.map((recent) => this.getSearchResultItem(recent, template, showPath));
-
         searchTerm = searchTerm.replace(this.getPrefix() + " ", "").trim();
 
+        if (!isPath(searchTerm)) {
+            return this.getRecentSearchResults(searchTerm, template, showPath);
+        } else {
+            return this.getFilesystemSearchResults(searchTerm, template);
+        }
+    }
+
+    private getRecentSearchResults(searchTerm: string, template: string, showPath: boolean): InstantSearchResultItems {
+        const searchResultItems = this.recents.map((recent) => this.getSearchResultItem(recent, template, showPath));
+
         if (searchTerm === "") {
             return {
                 after: searchResultItems,
@@ -280,6 +290,39 @@ export class VSCodeExtension implements Extension {
         };
     }
 
+    private instant: SearchResultItem[] = [];
+
+    private getFilesystemSearchResults(searchTerm: string, template: string): InstantSearchResultItems {
+        searchTerm = searchTerm.replace("~", (process.env as Record<string, string>).HOME);
+
+        if (this.fileSystemUtility.isDirectory(searchTerm)) {
+            const entries = this.fileSystemUtility.readDirectorySync(searchTerm, false);
+
+            this.instant = entries.map((e) => this.getFilePathItem(e, template, this.fileSystemUtility.isDirectory(e)));
+        }
+
+        return {
+            after: this.instant,
+            before: [],
+        };
+    }
+
+    private getFilePathItem(path: string, template: string, isDir: boolean): SearchResultItem {
+        return {
+            id: `vscode-filepath-${path}`,
+            name: Path.basename(path),
+            details: path,
+            description: isDir ? "Folder" : "File",
+            image: isDir ? this.getImage() : this.getDefaultFileImage(),
+            defaultAction: {
+                handlerId: "Commandline",
+                description: `Open ${path} in VSCode`,
+                argument: template.replace("%s", path),
+                hideWindowAfterInvocation: true,
+            },
+        };
+    }
+
     private getPrefix(): string {
         return this.settingsManager.getValue<string>(
             `extension[${this.id}].prefix`,
@@ -287,3 +330,11 @@ export class VSCodeExtension implements Extension {
         );
     }
 }
+
+export const isPath = (searchTerm: string | null | undefined) => {
+    if (!searchTerm) {
+        return false;
+    }
+    const windowMatch = searchTerm.match(/[A-Z]:.*/) !== null;
+    return searchTerm.startsWith("/") || searchTerm.startsWith("~") || windowMatch;
+};
diff --git a/src/main/Extensions/VSCode/VSCodeModule.ts b/src/main/Extensions/VSCode/VSCodeModule.ts
index 27f7d545..2041695f 100644
--- a/src/main/Extensions/VSCode/VSCodeModule.ts
+++ b/src/main/Extensions/VSCode/VSCodeModule.ts
@@ -12,6 +12,7 @@ export class VSCodeModule implements ExtensionModule {
                 moduleRegistry.get("Logger"),
                 moduleRegistry.get("SettingsManager"),
                 moduleRegistry.get("FileImageGenerator"),
+                moduleRegistry.get("FileSystemUtility"),
             ),
         };
     }


Test patch:
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
index 6f036ebd..46415742 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
@@ -171,6 +171,49 @@ describe(NodeJsFileSystemUtility, () => {
         });
     });
 
+    describe(NodeJsFileSystemUtility.prototype.readDirectorySync, () => {
+        it("should return a list of absolute folder and file paths when reading a folder", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+            const folderPath = join(tempFolderPath, "folder");
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(folderPath, "file2.txt");
+
+            await fileSystemUtility.createFolder(folderPath);
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = fileSystemUtility.readDirectorySync(tempFolderPath, false);
+
+            expect(actual).toEqual([filePath1, folderPath]);
+        });
+
+        it("should return a list of absolute folder and file paths when reading a folder recursively", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+            const folderPath = join(tempFolderPath, "folder");
+            const filePath1 = join(tempFolderPath, "file1.txt");
+            const filePath2 = join(folderPath, "file2.txt");
+
+            await fileSystemUtility.createFolder(folderPath);
+            await fileSystemUtility.writeTextFile("test", filePath1);
+            await fileSystemUtility.writeTextFile("test 2", filePath2);
+
+            const actual = fileSystemUtility.readDirectorySync(tempFolderPath, true);
+
+            expect(actual).toEqual([filePath1, folderPath, filePath2]);
+        });
+
+        it("should return an empty array when the given folder path is empty", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+            const folderPath = join(tempFolderPath, "folder");
+
+            await fileSystemUtility.createFolder(folderPath);
+
+            const actual = fileSystemUtility.readDirectorySync(folderPath);
+
+            expect(actual).toEqual([]);
+        });
+    });
+
     describe(NodeJsFileSystemUtility.prototype.readFile, () => {
         it("should read the contents of a file as a buffer", async () => {
             const fileSystemUtility = new NodeJsFileSystemUtility();
diff --git a/src/main/Extensions/VSCode/VSCodeExtension.test.ts b/src/main/Extensions/VSCode/VSCodeExtension.test.ts
new file mode 100644
index 00000000..2f16d3e4
--- /dev/null
+++ b/src/main/Extensions/VSCode/VSCodeExtension.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from "vitest";
+import { isPath } from "./VSCodeExtension";
+
+describe(isPath, () => {
+    it("should return true for absolute unix style absolute paths", () => {
+        expect(isPath("/home/developer/foo")).toBe(true);
+    });
+
+    it.each(["home/developer/foo", "", null, undefined, "scripts", "test.workspace", "file://path/to/uri"])(
+        'should return false for "%s" not starting with /',
+        (path) => {
+            expect(isPath(path)).toBe(false);
+        },
+    );
+    it.each(["C:\\Users\\developer\\foo"])('should return true for absolute windows paths "%s"', (path) => {
+        expect(isPath(path)).toBe(true);
+    });
+});