Repository: oliverschwendener/ueli
Pull request #1282 (2024-12-04 16:56:14)

refactor(InstantSearchResults): added the option to order instant search results
The current problem is that all instant search results go after the oridnary search results. For most extensions I assume that is the right behavior but for others it would be convenient to place the instant search results at the beginning of the search result list. See [this conversation](https://github.com/oliverschwendener/ueli/pull/1279#issuecomment-2516601653) as an example.

The behavior of all existing extensions should be unchanged, as all instant search results go **after** the regular search results.

@IrishBruse @NiewView Please confirm that your extensions still behave as expected.

IrishBruse: Im seeing some duplication
![image](https://github.com/user-attachments/assets/f0ff25a6-c56a-41a4-9a4b-b95479b83d5c)

This is the searchResult after typing `ray`
```
[
  {
    id: 'vscode-file:///home/econn/git/raylib',
    name: 'raylib',
    description: 'Folder',
    image: [Object],
    defaultAction: [Object]
  },
  {
    id: 'vscode-file:///home/econn/git/tray-notes',
    name: 'tray-notes',
    description: 'Folder',
    image: [Object],
    defaultAction: [Object]
  }
]
```

I suspect its due to this prior when typing `r`
My guess is its only removing the file one then it keeps duplicating itself?
```
    {
      id: 'vscode-file:///home/econn/git/core',
      name: 'core',
      description: 'Folder',
      image: [Object],
      defaultAction: [Object]
    },
    {
      id: 'vscode-file:///home/econn/git/core',
      name: 'core',
      description: 'File',
      image: [Object],
      defaultAction: [Object]
    },
```
IrishBruse: Ah seems to be a bug in my extension as its creating duplicate ids but you may still want to handle duplicates in case ill send in a pr to fix the bug to my extension
oliverschwendener: > Ah seems to be a bug in my extension

@IrishBruse Does this mean it is also a problem in the current `main` branch? Or was the problem introduced with the change of this PR?
IrishBruse: The bug is on main but it's different this makes it worse by duplicating the broken file\folder

Patch:
diff --git a/src/common/Core/ContextBridge.ts b/src/common/Core/ContextBridge.ts
index 0f59c692..37edc8c9 100644
--- a/src/common/Core/ContextBridge.ts
+++ b/src/common/Core/ContextBridge.ts
@@ -1,6 +1,7 @@
 import type { IpcRenderer, OpenDialogOptions, OpenDialogReturnValue, OpenExternalOptions } from "electron";
 import type { AboutUeli } from "./AboutUeli";
 import type { ExtensionInfo } from "./ExtensionInfo";
+import type { InstantSearchResultItems } from "./InstantSearchResultItems";
 import type { OperatingSystem } from "./OperatingSystem";
 import type { SearchResultItem } from "./SearchResultItem";
 import type { SearchResultItemAction } from "./SearchResultItemAction";
@@ -34,7 +35,7 @@ export type ContextBridge = {
     getExtensionSettingDefaultValue: <Value>(extensionId: string, settingKey: string) => Value;
     getFavorites: () => string[];
     getLogs: () => string[];
-    getInstantSearchResultItems: (searchTerm: string) => SearchResultItem[];
+    getInstantSearchResultItems: (searchTerm: string) => InstantSearchResultItems;
     getOperatingSystem: () => OperatingSystem;
     getSearchResultItems: () => SearchResultItem[];
     getSettingValue: <Value>(key: string, defaultValue: Value, isSensitive?: boolean) => Value;
diff --git a/src/common/Core/InstantSearchResultItems.ts b/src/common/Core/InstantSearchResultItems.ts
new file mode 100644
index 00000000..3b8c6a35
--- /dev/null
+++ b/src/common/Core/InstantSearchResultItems.ts
@@ -0,0 +1,11 @@
+import type { SearchResultItem } from "@common/Core";
+
+export const createEmptyInstantSearchResult = (): InstantSearchResultItems => ({
+    after: [],
+    before: [],
+});
+
+export type InstantSearchResultItems = {
+    after: SearchResultItem[];
+    before: SearchResultItem[];
+};
diff --git a/src/common/Core/index.ts b/src/common/Core/index.ts
index 338e8e85..7c79b2e1 100644
--- a/src/common/Core/index.ts
+++ b/src/common/Core/index.ts
@@ -3,6 +3,7 @@ export * from "./ContextBridge";
 export * as Extension from "./Extension";
 export * from "./ExtensionInfo";
 export * from "./FluentIcon";
+export * from "./InstantSearchResultItems";
 export * from "./OperatingSystem";
 export * from "./SearchResultItem";
 export * from "./SearchResultItemAction";
diff --git a/src/main/Core/Extension/Contract/Extension.ts b/src/main/Core/Extension/Contract/Extension.ts
index 52c85445..ee3adb84 100644
--- a/src/main/Core/Extension/Contract/Extension.ts
+++ b/src/main/Core/Extension/Contract/Extension.ts
@@ -1,4 +1,4 @@
-import type { SearchResultItem } from "@common/Core";
+import type { InstantSearchResultItems, SearchResultItem } from "@common/Core";
 import type { Image } from "@common/Core/Image";
 import type { Resources, Translations } from "@common/Core/Translator";
 
@@ -74,7 +74,7 @@ export interface Extension {
      * possible. For heavier operations use `getSearchResultItems`.
      * @param searchTerm The current search term.
      */
-    getInstantSearchResultItems?(searchTerm: string): SearchResultItem[];
+    getInstantSearchResultItems?(searchTerm: string): InstantSearchResultItems;
 
     /**
      * If you have a custom UI for your extension you can call it via `contextBridge.invokeExtension(arg)` with any
diff --git a/src/main/Core/ExtensionManager/ExtensionManager.ts b/src/main/Core/ExtensionManager/ExtensionManager.ts
index b3de7cc1..00582263 100644
--- a/src/main/Core/ExtensionManager/ExtensionManager.ts
+++ b/src/main/Core/ExtensionManager/ExtensionManager.ts
@@ -2,7 +2,7 @@ import type { ExtensionRegistry } from "@Core/ExtensionRegistry";
 import type { Logger } from "@Core/Logger";
 import type { SearchIndex } from "@Core/SearchIndex";
 import type { SettingsManager } from "@Core/SettingsManager";
-import type { SearchResultItem } from "@common/Core";
+import { createEmptyInstantSearchResult, type InstantSearchResultItems } from "@common/Core";
 import type { ScanCounter } from "./ScanCounter";
 
 export class ExtensionManager {
@@ -58,16 +58,20 @@ export class ExtensionManager {
         );
     }
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
-        const result: SearchResultItem[] = [];
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
+        const result: InstantSearchResultItems = createEmptyInstantSearchResult();
 
         for (const extension of this.getEnabledExtensions()) {
-            const searchResultItems = extension.getInstantSearchResultItems
+            const instantSearchResultItems: InstantSearchResultItems = extension.getInstantSearchResultItems
                 ? extension.getInstantSearchResultItems(searchTerm)
-                : [];
+                : createEmptyInstantSearchResult();
 
-            for (const searchResultItem of searchResultItems) {
-                result.push(searchResultItem);
+            for (const searchResultItem of instantSearchResultItems.after) {
+                result.after.push(searchResultItem);
+            }
+
+            for (const searchResultItem of instantSearchResultItems.before) {
+                result.before.push(searchResultItem);
             }
         }
 
diff --git a/src/main/Extensions/Calculator/CalculatorExtension.ts b/src/main/Extensions/Calculator/CalculatorExtension.ts
index 21a17d52..3b33e10c 100644
--- a/src/main/Extensions/Calculator/CalculatorExtension.ts
+++ b/src/main/Extensions/Calculator/CalculatorExtension.ts
@@ -1,7 +1,12 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { Extension } from "@Core/Extension";
 import type { SettingsManager } from "@Core/SettingsManager";
-import { createCopyToClipboardAction, type SearchResultItem } from "@common/Core";
+import {
+    createCopyToClipboardAction,
+    createEmptyInstantSearchResult,
+    type InstantSearchResultItems,
+    type SearchResultItem,
+} from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
 import { Calculator } from "./Calculator";
@@ -37,33 +42,36 @@ export class CalculatorExtension implements Extension {
         private readonly settingsManager: SettingsManager,
     ) {}
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         const result = this.getResultForExpression(searchTerm);
 
         if (result === undefined) {
-            return [];
+            return createEmptyInstantSearchResult();
         }
 
-        return [
-            {
-                name: result,
-                description: "Calculator",
-                descriptionTranslation: {
-                    key: "calculatorResult",
-                    namespace: "extension[Calculator]",
-                },
-                id: "calculator:instantResult",
-                image: this.getImage(),
-                defaultAction: createCopyToClipboardAction({
-                    textToCopy: result,
-                    description: "Copy result to clipboard",
+        return {
+            after: [
+                {
+                    name: result,
+                    description: "Calculator",
                     descriptionTranslation: {
-                        key: "copyResultToClipboard",
+                        key: "calculatorResult",
                         namespace: "extension[Calculator]",
                     },
-                }),
-            },
-        ];
+                    id: "calculator:instantResult",
+                    image: this.getImage(),
+                    defaultAction: createCopyToClipboardAction({
+                        textToCopy: result,
+                        description: "Copy result to clipboard",
+                        descriptionTranslation: {
+                            key: "copyResultToClipboard",
+                            namespace: "extension[Calculator]",
+                        },
+                    }),
+                },
+            ],
+            before: [],
+        };
     }
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
diff --git a/src/main/Extensions/ColorConverter/ColorConverterExtension.ts b/src/main/Extensions/ColorConverter/ColorConverterExtension.ts
index 1ccff18f..14b83544 100644
--- a/src/main/Extensions/ColorConverter/ColorConverterExtension.ts
+++ b/src/main/Extensions/ColorConverter/ColorConverterExtension.ts
@@ -1,4 +1,4 @@
-import { createCopyToClipboardAction, type SearchResultItem } from "@common/Core";
+import { createCopyToClipboardAction, type InstantSearchResultItems, type SearchResultItem } from "@common/Core";
 import type { Image } from "@common/Core/Image";
 import type { Resources, Translations } from "@common/Core/Translator";
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
@@ -71,26 +71,29 @@ export class ColorConverterExtension implements Extension {
         };
     }
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         const { t } = this.translator.createT(this.getI18nResources());
 
-        return this.colorConverter
-            .convertFromString(searchTerm)
-            .filter(({ format }) => this.getEnabledColorFormats().includes(format))
-            .map(({ format, value }) => ({
-                defaultAction: createCopyToClipboardAction({
-                    textToCopy: value,
-                    description: "Copy color to clipboard",
-                    descriptionTranslation: {
-                        key: "copyColorToClipboard",
-                        namespace: "extension[ColorConverter]",
-                    },
-                }),
-                description: t("color", { format }),
-                id: `color-${value}-${format}`,
-                image: this.getImage(),
-                name: value,
-            }));
+        return {
+            after: this.colorConverter
+                .convertFromString(searchTerm)
+                .filter(({ format }) => this.getEnabledColorFormats().includes(format))
+                .map(({ format, value }) => ({
+                    defaultAction: createCopyToClipboardAction({
+                        textToCopy: value,
+                        description: "Copy color to clipboard",
+                        descriptionTranslation: {
+                            key: "copyColorToClipboard",
+                            namespace: "extension[ColorConverter]",
+                        },
+                    }),
+                    description: t("color", { format }),
+                    id: `color-${value}-${format}`,
+                    image: this.getImage(),
+                    name: value,
+                })),
+            before: [],
+        };
     }
 
     private getEnabledColorFormats(): string[] {
diff --git a/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts b/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
index 80118362..de003a2b 100644
--- a/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
+++ b/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
@@ -1,4 +1,9 @@
-import { createCopyToClipboardAction, type SearchResultItem } from "@common/Core";
+import {
+    createCopyToClipboardAction,
+    createEmptyInstantSearchResult,
+    type InstantSearchResultItems,
+    type SearchResultItem,
+} from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
@@ -39,7 +44,7 @@ export class CurrencyConversion implements Extension {
         private readonly assetPathResolver: AssetPathResolver,
     ) {}
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         const parts = searchTerm.trim().split(" ");
 
         const validators = [
@@ -55,7 +60,7 @@ export class CurrencyConversion implements Extension {
 
         for (const validator of validators) {
             if (!validator()) {
-                return [];
+                return createEmptyInstantSearchResult();
             }
         }
 
@@ -65,26 +70,29 @@ export class CurrencyConversion implements Extension {
 
         const conversionResult = convert({ value, base, target, rates: this.rates });
 
-        return [
-            {
-                defaultAction: createCopyToClipboardAction({
-                    textToCopy: conversionResult.result.toFixed(2),
+        return {
+            after: [],
+            before: [
+                {
+                    defaultAction: createCopyToClipboardAction({
+                        textToCopy: conversionResult.result.toFixed(2),
+                        description: "Currency Conversion",
+                        descriptionTranslation: {
+                            key: "copyToClipboard",
+                            namespace: CurrencyConversion.translationNamespace,
+                        },
+                    }),
                     description: "Currency Conversion",
                     descriptionTranslation: {
-                        key: "copyToClipboard",
+                        key: "currencyConversion",
                         namespace: CurrencyConversion.translationNamespace,
                     },
-                }),
-                description: "Currency Conversion",
-                descriptionTranslation: {
-                    key: "currencyConversion",
-                    namespace: CurrencyConversion.translationNamespace,
+                    id: `currency-conversion:instant-result`,
+                    image: this.getImage(),
+                    name: `${conversionResult.result.toFixed(2)} ${target.toUpperCase()}`,
                 },
-                id: `currency-conversion:instant-result`,
-                image: this.getImage(),
-                name: `${conversionResult.result.toFixed(2)} ${target.toUpperCase()}`,
-            },
-        ];
+            ],
+        };
     }
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
diff --git a/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.ts b/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.ts
index 54508d07..b8e435e5 100644
--- a/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.ts
+++ b/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.ts
@@ -2,7 +2,7 @@ import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { Extension } from "@Core/Extension";
 import type { UrlImageGenerator } from "@Core/ImageGenerator";
 import type { SettingsManager } from "@Core/SettingsManager";
-import type { SearchResultItem } from "@common/Core";
+import { createEmptyInstantSearchResult, type InstantSearchResultItems, type SearchResultItem } from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
 import type { CustomSearchEngineSetting, Settings } from "@common/Extensions/CustomWebSearch";
@@ -49,7 +49,7 @@ export class CustomWebSearchExtension implements Extension {
         };
     }
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         const customSearchEngines = this.settingsManager.getValue<CustomSearchEngineSetting[]>(
             getExtensionSettingKey(this.id, "customSearchEngines"),
             this.getSettingDefaultValue("customSearchEngines"),
@@ -58,10 +58,13 @@ export class CustomWebSearchExtension implements Extension {
         const selectedSearchEngines = customSearchEngines.filter((engine) => searchTerm.startsWith(engine.prefix));
 
         if (selectedSearchEngines.length === 0) {
-            return [];
+            return createEmptyInstantSearchResult();
         }
 
-        return selectedSearchEngines.map((engine) => this.createInstantSearchResultItem(engine, searchTerm));
+        return {
+            after: selectedSearchEngines.map((engine) => this.createInstantSearchResultItem(engine, searchTerm)),
+            before: [],
+        };
     }
 
     public isSupported(): boolean {
diff --git a/src/main/Extensions/TerminalLauncher/TerminalLauncherExtension.ts b/src/main/Extensions/TerminalLauncher/TerminalLauncherExtension.ts
index c5050316..c4335d24 100644
--- a/src/main/Extensions/TerminalLauncher/TerminalLauncherExtension.ts
+++ b/src/main/Extensions/TerminalLauncher/TerminalLauncherExtension.ts
@@ -3,7 +3,12 @@ import type { Extension } from "@Core/Extension";
 import type { SettingsManager } from "@Core/SettingsManager";
 import type { TerminalRegistry } from "@Core/Terminal";
 import type { Translator } from "@Core/Translator";
-import type { OperatingSystem, SearchResultItem } from "@common/Core";
+import {
+    createEmptyInstantSearchResult,
+    type InstantSearchResultItems,
+    type OperatingSystem,
+    type SearchResultItem,
+} from "@common/Core";
 import type { Image } from "@common/Core/Image";
 import type { ActionArgument } from "./ActionArgument";
 import type { Settings } from "./Settings";
@@ -82,28 +87,31 @@ export class TerminalLauncherExtension implements Extension {
         };
     }
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         if (!searchTerm.startsWith(this.getPrefix()) || searchTerm.replace(this.getPrefix(), "").trim().length === 0) {
-            return [];
+            return createEmptyInstantSearchResult();
         }
 
         const { t } = this.translator.createT(this.getI18nResources());
 
         const command = this.extractCommandFromSearchTerm(searchTerm);
 
-        return this.getEnabledTerminalIds().map((terminalId) => ({
-            defaultAction: {
-                argument: JSON.stringify(<ActionArgument>{ command, terminalId }),
-                description: t("defaultActionDescription", { terminalId }),
-                handlerId: "LaunchTerminalActionHandler",
-                fluentIcon: "WindowConsoleRegular",
-                hideWindowAfterInvocation: true,
-            },
-            description: t("searchResultItemDescription", { terminalId }),
-            id: `[${this.id}][instantSearchResultItem][${terminalId}]`,
-            image: this.getTerminalImage(terminalId),
-            name: command,
-        }));
+        return {
+            after: this.getEnabledTerminalIds().map((terminalId) => ({
+                defaultAction: {
+                    argument: JSON.stringify(<ActionArgument>{ command, terminalId }),
+                    description: t("defaultActionDescription", { terminalId }),
+                    handlerId: "LaunchTerminalActionHandler",
+                    fluentIcon: "WindowConsoleRegular",
+                    hideWindowAfterInvocation: true,
+                },
+                description: t("searchResultItemDescription", { terminalId }),
+                id: `[${this.id}][instantSearchResultItem][${terminalId}]`,
+                image: this.getTerminalImage(terminalId),
+                name: command,
+            })),
+            before: [],
+        };
     }
 
     private getPrefix(): string {
diff --git a/src/main/Extensions/VSCode/VSCodeExtension.ts b/src/main/Extensions/VSCode/VSCodeExtension.ts
index 9c88e24f..140a72f3 100644
--- a/src/main/Extensions/VSCode/VSCodeExtension.ts
+++ b/src/main/Extensions/VSCode/VSCodeExtension.ts
@@ -3,7 +3,12 @@ import type { Extension } from "@Core/Extension";
 import type { FileImageGenerator } from "@Core/ImageGenerator";
 import type { Logger } from "@Core/Logger";
 import type { SettingsManager } from "@Core/SettingsManager";
-import type { OperatingSystem, SearchResultItem } from "@common/Core";
+import {
+    createEmptyInstantSearchResult,
+    type InstantSearchResultItems,
+    type OperatingSystem,
+    type SearchResultItem,
+} from "@common/Core";
 import type { Image } from "@common/Core/Image";
 import type { SearchEngineId } from "@common/Core/Search";
 import { searchFilter } from "@common/Core/Search/SearchFilter";
@@ -214,30 +219,36 @@ export class VSCodeExtension implements Extension {
         };
     }
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         if (this.getPrefix().trim() !== "" && !searchTerm.startsWith(this.getPrefix() + " ")) {
-            return [];
+            return createEmptyInstantSearchResult();
         }
 
         searchTerm = searchTerm.replace(this.getPrefix() + " ", "").trim();
 
         if (searchTerm && searchTerm === "") {
-            return this.recents;
+            return {
+                after: this.recents,
+                before: [],
+            };
         }
 
         const fuzziness = this.settingsManager.getValue<number>("searchEngine.fuzziness", 0.5);
         const maxSearchResultItems = this.settingsManager.getValue<number>("searchEngine.maxResultLength", 50);
         const searchEngineId = this.settingsManager.getValue<SearchEngineId>("searchEngine.id", "fuzzysort");
 
-        return searchFilter(
-            {
-                searchResultItems: this.recents,
-                searchTerm,
-                fuzziness,
-                maxSearchResultItems,
-            },
-            searchEngineId,
-        );
+        return {
+            after: searchFilter(
+                {
+                    searchResultItems: this.recents,
+                    searchTerm,
+                    fuzziness,
+                    maxSearchResultItems,
+                },
+                searchEngineId,
+            ),
+            before: [],
+        };
     }
 
     private getPrefix(): string {
diff --git a/src/main/Extensions/WebSearch/WebSearchExtension.ts b/src/main/Extensions/WebSearch/WebSearchExtension.ts
index 1efa9a84..11e8ce3d 100644
--- a/src/main/Extensions/WebSearch/WebSearchExtension.ts
+++ b/src/main/Extensions/WebSearch/WebSearchExtension.ts
@@ -1,7 +1,13 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { Extension } from "@Core/Extension";
 import type { SettingsManager } from "@Core/SettingsManager";
-import { createInvokeExtensionAction, createOpenUrlSearchResultAction, type SearchResultItem } from "@common/Core";
+import {
+    createEmptyInstantSearchResult,
+    createInvokeExtensionAction,
+    createOpenUrlSearchResultAction,
+    type InstantSearchResultItems,
+    type SearchResultItem,
+} from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
 import type { Settings } from "./Settings";
@@ -33,7 +39,7 @@ export class WebSearchExtension implements Extension {
         private readonly webSearchEngines: WebSearchEngine[],
     ) {}
 
-    public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
+    public getInstantSearchResultItems(searchTerm: string): InstantSearchResultItems {
         const showInstantSearchResult = this.settingsManager.getValue(
             getExtensionSettingKey(this.id, "showInstantSearchResult"),
             this.getSettingDefaultValue("showInstantSearchResult"),
@@ -47,10 +53,13 @@ export class WebSearchExtension implements Extension {
 
             const webSearchEngine = this.getCurrentWebSearchEngine();
 
-            return [this.getInstantSearchResultItem(searchTerm, locale, webSearchEngine)];
+            return {
+                after: [this.getInstantSearchResultItem(searchTerm, locale, webSearchEngine)],
+                before: [],
+            };
         }
 
-        return [];
+        return createEmptyInstantSearchResult();
     }
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
diff --git a/src/renderer/Core/Search/Helpers/getSearchResult.ts b/src/renderer/Core/Search/Helpers/getSearchResult.ts
index 54bb48dd..fc99f366 100644
--- a/src/renderer/Core/Search/Helpers/getSearchResult.ts
+++ b/src/renderer/Core/Search/Helpers/getSearchResult.ts
@@ -1,4 +1,4 @@
-import type { SearchResultItem } from "@common/Core";
+import type { InstantSearchResultItems, SearchResultItem } from "@common/Core";
 import { searchFilter, SearchResultItemFilter, type SearchEngineId } from "@common/Core/Search";
 
 export const getSearchResult = ({
@@ -14,7 +14,7 @@ export const getSearchResult = ({
     searchEngineId: SearchEngineId;
     favoriteSearchResultItemIds: string[];
     excludedSearchResultItemIds: string[];
-    instantSearchResultItems: SearchResultItem[];
+    instantSearchResultItems: InstantSearchResultItems;
     searchResultItems: SearchResultItem[];
     searchTerm: string;
     fuzziness: number;
@@ -36,8 +36,9 @@ export const getSearchResult = ({
         return {
             favorites: SearchResultItemFilter.createFrom(searchFilterItems).pick(favoriteSearchResultItemIds).get(),
             searchResults: [
+                ...instantSearchResultItems.before,
                 ...SearchResultItemFilter.createFrom(searchFilterItems).exclude(favoriteSearchResultItemIds).get(),
-                ...instantSearchResultItems,
+                ...instantSearchResultItems.after,
             ],
         };
     } else {


Test patch:
diff --git a/src/main/Core/ExtensionManager/ExtensionManager.test.ts b/src/main/Core/ExtensionManager/ExtensionManager.test.ts
index 2fcacc7a..017adf2c 100644
--- a/src/main/Core/ExtensionManager/ExtensionManager.test.ts
+++ b/src/main/Core/ExtensionManager/ExtensionManager.test.ts
@@ -3,7 +3,7 @@ import type { ExtensionRegistry } from "@Core/ExtensionRegistry";
 import type { Logger } from "@Core/Logger";
 import type { SearchIndex } from "@Core/SearchIndex";
 import type { SettingsManager } from "@Core/SettingsManager";
-import type { SearchResultItem } from "@common/Core";
+import type { InstantSearchResultItems, SearchResultItem } from "@common/Core";
 import { describe, expect, it, vi } from "vitest";
 import { ExtensionManager } from "./ExtensionManager";
 import type { ScanCounter } from "./ScanCounter";
@@ -96,18 +96,18 @@ describe(ExtensionManager, () => {
         const extension1 = <Extension>{
             id: "extension1",
             isSupported: () => true,
-            getInstantSearchResultItems: (s) => {
+            getInstantSearchResultItems: (s): InstantSearchResultItems => {
                 getInstantSearchResultItemsMock(s);
-                return [getInstantSearchResultItem("extension1")];
+                return { after: [getInstantSearchResultItem("extension1")], before: [] };
             },
         };
 
         const extension2 = <Extension>{
             id: "extension2",
             isSupported: () => true,
-            getInstantSearchResultItems: (s) => {
+            getInstantSearchResultItems: (s): InstantSearchResultItems => {
                 getInstantSearchResultItemsMock(s);
-                return [getInstantSearchResultItem("extension2")];
+                return { before: [getInstantSearchResultItem("extension2")], after: [] };
             },
         };
 
@@ -123,9 +123,10 @@ describe(ExtensionManager, () => {
             <ScanCounter>{},
         );
 
-        const actual = extensionManager.getInstantSearchResultItems("search term");
+        const { before, after } = extensionManager.getInstantSearchResultItems("search term");
 
-        expect(actual).toEqual([getInstantSearchResultItem("extension1"), getInstantSearchResultItem("extension2")]);
+        expect(after).toEqual([getInstantSearchResultItem("extension1")]);
+        expect(before).toEqual([getInstantSearchResultItem("extension2")]);
         expect(getInstantSearchResultItemsMock).toHaveBeenCalledTimes(2);
         expect(getInstantSearchResultItemsMock).toHaveBeenCalledWith("search term");
     });
diff --git a/src/main/Extensions/ColorConverter/ColorConverterExtension.test.ts b/src/main/Extensions/ColorConverter/ColorConverterExtension.test.ts
index 5dd38bf3..20eb0f22 100644
--- a/src/main/Extensions/ColorConverter/ColorConverterExtension.test.ts
+++ b/src/main/Extensions/ColorConverter/ColorConverterExtension.test.ts
@@ -99,15 +99,17 @@ describe(ColorConverterExtension, () => {
                 colorConverter,
             );
 
-            const actual = colorConverterExtension.getInstantSearchResultItems("#fff");
+            const { before, after } = colorConverterExtension.getInstantSearchResultItems("#fff");
+
+            expect(before).toEqual([]);
 
             expect(colorConverter.convertFromString).toHaveBeenCalledOnce();
             expect(colorConverter.convertFromString).toHaveBeenCalledWith("#fff");
 
-            expect(actual.length).toBe(2);
-            expect(actual.map((a) => a.name)).toEqual(["#FFFFFF", "rgb(255, 255, 255)"]);
+            expect(after.length).toBe(2);
+            expect(after.map((a) => a.name)).toEqual(["#FFFFFF", "rgb(255, 255, 255)"]);
 
-            expect(actual.map(({ image }) => image)).toEqual([
+            expect(after.map(({ image }) => image)).toEqual([
                 { url: "file://color-converter.png" },
                 { url: "file://color-converter.png" },
             ]);
diff --git a/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts b/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts
index 1676272c..014d2adf 100644
--- a/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts
+++ b/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts
@@ -1,3 +1,4 @@
+import { createEmptyInstantSearchResult } from "@common/Core";
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { SettingsManager } from "@Core/SettingsManager";
 import type { Net } from "electron";
@@ -35,11 +36,12 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = rates;
 
-            const actual = currencyConversion.getInstantSearchResultItems(userInput);
+            const { before, after } = currencyConversion.getInstantSearchResultItems(userInput);
 
-            expect(actual.length).toEqual(1);
-            expect(actual[0].name).toEqual(expectedResult);
-            expect(actual[0].image).toEqual({ url: `file://${imageFilePath}` });
+            expect(after).toEqual([]);
+            expect(before.length).toEqual(1);
+            expect(before[0].name).toEqual(expectedResult);
+            expect(before[0].image).toEqual({ url: `file://${imageFilePath}` });
             expect(getExtensionAssetPathMock).toHaveBeenCalledWith(currencyConversion.id, "currency-conversion.png");
         };
 
@@ -48,9 +50,13 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = { chf: { usd: 2 } };
 
-            expect(currencyConversion.getInstantSearchResultItems("1")).toEqual([]);
-            expect(currencyConversion.getInstantSearchResultItems("1 CHF to")).toEqual([]);
-            expect(currencyConversion.getInstantSearchResultItems("1 CHF to USD else")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("1")).toEqual(createEmptyInstantSearchResult());
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF to")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF to USD else")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should return empty array when first part in user input is not numerical", () => {
@@ -58,7 +64,9 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = { chf: { usd: 2 } };
 
-            expect(currencyConversion.getInstantSearchResultItems("abc CHF to USD")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("abc CHF to USD")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should return empty array when base currency does not exist", () => {
@@ -66,7 +74,9 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = { chf: { usd: 2 } };
 
-            expect(currencyConversion.getInstantSearchResultItems("1 EUR to USD")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("1 EUR to USD")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should return empty array when second part of user input is not 'in' or 'to'", () => {
@@ -74,8 +84,12 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = { chf: { usd: 2 } };
 
-            expect(currencyConversion.getInstantSearchResultItems("1 CHF inn USD")).toEqual([]);
-            expect(currencyConversion.getInstantSearchResultItems("1 CHF t USD")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF inn USD")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF t USD")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should return empty array when target currency is not found", () => {
@@ -83,7 +97,9 @@ describe(CurrencyConversion, () => {
 
             currencyConversion["rates"] = { chf: { usd: 2 } };
 
-            expect(currencyConversion.getInstantSearchResultItems("1 CHF to EUR")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF to EUR")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should convert currencies based on the rates when user input matches expected pattern", () => {
diff --git a/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.test.ts b/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.test.ts
index 6d44390f..1a74785d 100644
--- a/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.test.ts
+++ b/src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.test.ts
@@ -1,3 +1,4 @@
+import { createEmptyInstantSearchResult } from "@common/Core";
 import type { CustomSearchEngineSetting } from "@common/Extensions/CustomWebSearch";
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { UrlImageGenerator } from "@Core/ImageGenerator/UrlImageGenerator";
@@ -67,7 +68,7 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            expect(customWebSearch.getInstantSearchResultItems("1")).toEqual([]);
+            expect(customWebSearch.getInstantSearchResultItems("1")).toEqual(createEmptyInstantSearchResult());
         });
 
         it("should return empty array when user input does not contain any prefix", () => {
@@ -77,10 +78,12 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            expect(customWebSearch.getInstantSearchResultItems("1")).toEqual([]);
-            expect(customWebSearch.getInstantSearchResultItems("test")).toEqual([]);
-            expect(customWebSearch.getInstantSearchResultItems("wik test")).toEqual([]);
-            expect(customWebSearch.getInstantSearchResultItems("gooooooogle test")).toEqual([]);
+            expect(customWebSearch.getInstantSearchResultItems("1")).toEqual(createEmptyInstantSearchResult());
+            expect(customWebSearch.getInstantSearchResultItems("test")).toEqual(createEmptyInstantSearchResult());
+            expect(customWebSearch.getInstantSearchResultItems("wik test")).toEqual(createEmptyInstantSearchResult());
+            expect(customWebSearch.getInstantSearchResultItems("gooooooogle test")).toEqual(
+                createEmptyInstantSearchResult(),
+            );
         });
 
         it("should replace query in search engine url", () => {
@@ -90,10 +93,12 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            const results = customWebSearch.getInstantSearchResultItems("google?testinput");
-            expect(results).toHaveLength(1);
-            expect(results[0].defaultAction.handlerId).toEqual("Url");
-            expect(results[0].defaultAction.argument).toEqual("https://ueli.app/google/testinput");
+            const { after, before } = customWebSearch.getInstantSearchResultItems("google?testinput");
+
+            expect(before).toEqual([]);
+            expect(after).toHaveLength(1);
+            expect(after[0].defaultAction.handlerId).toEqual("Url");
+            expect(after[0].defaultAction.argument).toEqual("https://ueli.app/google/testinput");
         });
 
         it("should trim spaces in search query input", () => {
@@ -103,10 +108,12 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            const results = customWebSearch.getInstantSearchResultItems("wiki testinput");
-            expect(results).toHaveLength(1);
-            expect(results[0].defaultAction.handlerId).toEqual("Url");
-            expect(results[0].defaultAction.argument).toEqual("https://ueli.app/wiki/testinput");
+            const { after, before } = customWebSearch.getInstantSearchResultItems("wiki testinput");
+
+            expect(before).toEqual([]);
+            expect(after).toHaveLength(1);
+            expect(after[0].defaultAction.handlerId).toEqual("Url");
+            expect(after[0].defaultAction.argument).toEqual("https://ueli.app/wiki/testinput");
         });
 
         it("should only encode query, if setting is enabled", () => {
@@ -117,14 +124,16 @@ describe(CustomWebSearchExtension, () => {
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
             const resultsEnabled = customWebSearch.getInstantSearchResultItems("wikitest input");
-            expect(resultsEnabled).toHaveLength(1);
-            expect(resultsEnabled[0].defaultAction.handlerId).toEqual("Url");
-            expect(resultsEnabled[0].defaultAction.argument).toEqual("https://ueli.app/wiki/test%20input");
+            expect(resultsEnabled.before).toEqual([]);
+            expect(resultsEnabled.after).toHaveLength(1);
+            expect(resultsEnabled.after[0].defaultAction.handlerId).toEqual("Url");
+            expect(resultsEnabled.after[0].defaultAction.argument).toEqual("https://ueli.app/wiki/test%20input");
 
             const resultsDisabled = customWebSearch.getInstantSearchResultItems("google?test input");
-            expect(resultsDisabled).toHaveLength(1);
-            expect(resultsDisabled[0].defaultAction.handlerId).toEqual("Url");
-            expect(resultsDisabled[0].defaultAction.argument).toEqual("https://ueli.app/google/test input");
+            expect(resultsDisabled.before).toEqual([]);
+            expect(resultsDisabled.after).toHaveLength(1);
+            expect(resultsDisabled.after[0].defaultAction.handlerId).toEqual("Url");
+            expect(resultsDisabled.after[0].defaultAction.argument).toEqual("https://ueli.app/google/test input");
         });
 
         it("should replace query, even if it is part of the domain", () => {
@@ -134,10 +143,12 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            const results = customWebSearch.getInstantSearchResultItems("domain testinput");
-            expect(results).toHaveLength(1);
-            expect(results[0].defaultAction.handlerId).toEqual("Url");
-            expect(results[0].defaultAction.argument).toEqual("https://testinput.ueli.app/test");
+            const { after, before } = customWebSearch.getInstantSearchResultItems("domain testinput");
+
+            expect(before).toEqual([]);
+            expect(after).toHaveLength(1);
+            expect(after[0].defaultAction.handlerId).toEqual("Url");
+            expect(after[0].defaultAction.argument).toEqual("https://testinput.ueli.app/test");
         });
 
         it("should allow multiple search results, when multiple search engines match", () => {
@@ -147,12 +158,14 @@ describe(CustomWebSearchExtension, () => {
             };
             const customWebSearch = new CustomWebSearchExtension(assetPathResolver, settingsManager, urlImageGenerator);
 
-            const results = customWebSearch.getInstantSearchResultItems("genius testinput");
-            expect(results).toHaveLength(2);
-            expect(results[0].defaultAction.handlerId).toEqual("Url");
-            expect(results[0].defaultAction.argument).toEqual("https://ueli.app/g/enius%20testinput");
-            expect(results[1].defaultAction.handlerId).toEqual("Url");
-            expect(results[1].defaultAction.argument).toEqual("https://ueli.app/genius/testinput");
+            const { after, before } = customWebSearch.getInstantSearchResultItems("genius testinput");
+
+            expect(before).toEqual([]);
+            expect(after).toHaveLength(2);
+            expect(after[0].defaultAction.handlerId).toEqual("Url");
+            expect(after[0].defaultAction.argument).toEqual("https://ueli.app/g/enius%20testinput");
+            expect(after[1].defaultAction.handlerId).toEqual("Url");
+            expect(after[1].defaultAction.argument).toEqual("https://ueli.app/genius/testinput");
         });
     });
 });
diff --git a/src/main/Extensions/WebSearch/WebSearchExtension.test.ts b/src/main/Extensions/WebSearch/WebSearchExtension.test.ts
index ee891f6b..6e2b6ef2 100644
--- a/src/main/Extensions/WebSearch/WebSearchExtension.test.ts
+++ b/src/main/Extensions/WebSearch/WebSearchExtension.test.ts
@@ -1,6 +1,11 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { SettingsManager } from "@Core/SettingsManager";
-import { createInvokeExtensionAction, createOpenUrlSearchResultAction, type SearchResultItem } from "@common/Core";
+import {
+    createEmptyInstantSearchResult,
+    createInvokeExtensionAction,
+    createOpenUrlSearchResultAction,
+    type SearchResultItem,
+} from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
 import { describe, expect, it, vi } from "vitest";
@@ -16,8 +21,9 @@ describe(WebSearchExtension, () => {
             <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) },
             [],
         );
-        expect(webSearchExtension.getInstantSearchResultItems("")).toEqual([]);
-        expect(webSearchExtension.getInstantSearchResultItems(" ")).toEqual([]);
+
+        expect(webSearchExtension.getInstantSearchResultItems("")).toEqual(createEmptyInstantSearchResult());
+        expect(webSearchExtension.getInstantSearchResultItems(" ")).toEqual(createEmptyInstantSearchResult());
 
         expect(getValueMock).toHaveBeenCalledWith(
             getExtensionSettingKey("WebSearch", "showInstantSearchResult"),
@@ -34,7 +40,7 @@ describe(WebSearchExtension, () => {
             [],
         );
 
-        expect(webSearchExtension.getInstantSearchResultItems("blub")).toEqual([]);
+        expect(webSearchExtension.getInstantSearchResultItems("blub")).toEqual(createEmptyInstantSearchResult());
 
         expect(getValueMock).toHaveBeenCalledWith(
             getExtensionSettingKey("WebSearch", "showInstantSearchResult"),
@@ -76,15 +82,18 @@ describe(WebSearchExtension, () => {
 
         const webSearchExtension = new WebSearchExtension(assetPathResolver, settingsManager, [webSearchEngine]);
 
-        expect(webSearchExtension.getInstantSearchResultItems("my search term")).toEqual([
-            <SearchResultItem>{
-                defaultAction: createOpenUrlSearchResultAction({ url: "mySearchUrl" }),
-                description: "searchEngine1",
-                id: "search-searchEngine1",
-                image: { url: "file://myAssetFilePath" },
-                name: `Search "my search term"`,
-            },
-        ]);
+        expect(webSearchExtension.getInstantSearchResultItems("my search term")).toEqual({
+            after: [
+                <SearchResultItem>{
+                    defaultAction: createOpenUrlSearchResultAction({ url: "mySearchUrl" }),
+                    description: "searchEngine1",
+                    id: "search-searchEngine1",
+                    image: { url: "file://myAssetFilePath" },
+                    name: `Search "my search term"`,
+                },
+            ],
+            before: [],
+        });
 
         expect(getWebSearchEngineMock).toHaveBeenCalledWith(
             getExtensionSettingKey("WebSearch", "searchEngine"),
diff --git a/src/renderer/Core/Search/Helpers/getSearchResult.test.ts b/src/renderer/Core/Search/Helpers/getSearchResult.test.ts
index 672f5877..7142e565 100644
--- a/src/renderer/Core/Search/Helpers/getSearchResult.test.ts
+++ b/src/renderer/Core/Search/Helpers/getSearchResult.test.ts
@@ -1,14 +1,21 @@
-import type { SearchResultItem } from "@common/Core";
+import type { InstantSearchResultItems, SearchResultItem } from "@common/Core";
 import { describe, expect, it } from "vitest";
 import { getSearchResult } from "./getSearchResult";
 
 describe(getSearchResult, () => {
     it("should return an array of instant search result items, favorites and other matches if a search term is given", () => {
-        const instantSearchResultItems = <SearchResultItem[]>[
-            { id: "instant1", name: "Instant Item 1" },
-            { id: "instant2", name: "Instant Item 2" },
-            { id: "instant3", name: "Instant Item 3" },
-        ];
+        const instantSearchResultItems: InstantSearchResultItems = {
+            before: <SearchResultItem[]>[
+                { id: "instant1-before", name: "Instant Item 1 before" },
+                { id: "instant2-before", name: "Instant Item 2 before" },
+                { id: "instant3-before", name: "Instant Item 3 before" },
+            ],
+            after: <SearchResultItem[]>[
+                { id: "instant1-after", name: "Instant Item 1 after" },
+                { id: "instant2-after", name: "Instant Item 2 after" },
+                { id: "instant3-after", name: "Instant Item 3 after" },
+            ],
+        };
 
         const searchResultItems = <SearchResultItem[]>[
             { id: "item1", name: "Item 1" },
@@ -33,7 +40,11 @@ describe(getSearchResult, () => {
 
         expect(actual).toEqual({
             favorites: [{ id: "item3", name: "Item 3" }],
-            searchResults: [{ id: "item2", name: "Item 2" }, ...instantSearchResultItems],
+            searchResults: [
+                ...instantSearchResultItems.before,
+                { id: "item2", name: "Item 2" },
+                ...instantSearchResultItems.after,
+            ],
         });
     });
 
@@ -48,11 +59,18 @@ describe(getSearchResult, () => {
             searchEngineId: "fuzzysort",
             favoriteSearchResultItemIds: ["item2", "item3"],
             excludedSearchResultItemIds: ["item1"],
-            instantSearchResultItems: <SearchResultItem[]>[
-                { id: "instant1", name: "Instant Item 1" },
-                { id: "instant2", name: "Instant Item 2" },
-                { id: "instant3", name: "Instant Item 3" },
-            ],
+            instantSearchResultItems: {
+                before: <SearchResultItem[]>[
+                    { id: "instant1-before", name: "Instant Item 1 before" },
+                    { id: "instant2-before", name: "Instant Item 2 before" },
+                    { id: "instant3-before", name: "Instant Item 3 before" },
+                ],
+                after: <SearchResultItem[]>[
+                    { id: "instant1-after", name: "Instant Item 1 after" },
+                    { id: "instant2-after", name: "Instant Item 2 after" },
+                    { id: "instant3-after", name: "Instant Item 3 after" },
+                ],
+            },
             searchResultItems: [item5, item1, item3, item2, item4],
             searchTerm: "",
             fuzziness: 0, // will be ignored