From c03ef64906e05cced0ba7ce8ca366fdaa9af86f0 Mon Sep 17 00:00:00 2001
From: filip131311 <159789821+filip131311@users.noreply.github.com>
Date: Tue, 26 Mar 2024 18:16:55 +0100
Subject: [PATCH] Preview in Activity Bar (#22)
This is a new feature:
- you can now choose in settings to mount your Preview to activity bar
instead of opening it in a tab.
- when you change settings, current preview is going to be disposed
---------
Co-authored-by: Filip Andrzej Kaminski
---
packages/vscode-extension/assets/logo.svg | 3 +
packages/vscode-extension/package.json | 27 ++-
packages/vscode-extension/src/extension.ts | 49 ++++-
.../src/panels/SidepanelViewProvider.ts | 63 +++++++
.../vscode-extension/src/panels/Tabpanel.ts | 82 +++++++++
...{PreviewsPanel.ts => WebviewController.ts} | 169 +++++-------------
6 files changed, 263 insertions(+), 130 deletions(-)
create mode 100644 packages/vscode-extension/assets/logo.svg
create mode 100644 packages/vscode-extension/src/panels/SidepanelViewProvider.ts
create mode 100644 packages/vscode-extension/src/panels/Tabpanel.ts
rename packages/vscode-extension/src/panels/{PreviewsPanel.ts => WebviewController.ts} (52%)
diff --git a/packages/vscode-extension/assets/logo.svg b/packages/vscode-extension/assets/logo.svg
new file mode 100644
index 000000000..3d357f05f
--- /dev/null
+++ b/packages/vscode-extension/assets/logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json
index 001fe9668..c0bffa094 100644
--- a/packages/vscode-extension/package.json
+++ b/packages/vscode-extension/package.json
@@ -51,15 +51,40 @@
"scope": "window",
"default": null,
"description": "Location of the React Native application root folder relative to the workspace workspace. This is used for monorepo type setups when the workspace root is not the root of the React Native project. The IDE extension tries to locate the React Native application root automatically, but in case it failes to do so (i.e. there are multiple applications defined in the workspace), you can use this setting to override the location."
+ },
+ "ReactNativeIDE.showPanelInActivityBar":{
+ "type": "boolean",
+ "scope": "window",
+ "default": false,
+ "description": "This option alows you to move IDE Panel to Activity Bar. (warning: if you currently have it open it is going to be close after ajusting that setting.)"
}
}
},
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "ReactNativeIDE",
+ "title": "React Native IDE",
+ "icon": "assets/logo.svg"
+ }
+ ]
+ },
+ "views": {
+ "ReactNativeIDE": [
+ {
+ "type": "webview",
+ "id": "ReactNativeIDE.view",
+ "name": "IDE Panel",
+ "when": "config.ReactNativeIDE.showPanelInActivityBar"
+ }
+ ]
+ },
"menus": {
"editor/title": [
{
"command": "RNIDE.openPanel",
"group": "navigation",
- "when": "RNIDE.extensionIsActive && !RNIDE.panelIsOpen"
+ "when": "RNIDE.extensionIsActive && !RNIDE.panelIsOpen && !config.ReactNativeIDE.showPanelInActivityBar"
}
]
},
diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts
index 1dc880595..35d0171c0 100644
--- a/packages/vscode-extension/src/extension.ts
+++ b/packages/vscode-extension/src/extension.ts
@@ -8,19 +8,26 @@ import {
ExtensionContext,
ExtensionMode,
DebugConfigurationProviderTriggerKind,
+ ConfigurationChangeEvent,
} from "vscode";
-import { PreviewsPanel } from "./panels/PreviewsPanel";
+import { Tabpanel } from "./panels/Tabpanel";
import { PreviewCodeLensProvider } from "./providers/PreviewCodeLensProvider";
import { DebugConfigProvider } from "./providers/DebugConfigProvider";
import { DebugAdapterDescriptorFactory } from "./debugging/DebugAdapterDescriptorFactory";
import { Logger, enableDevModeLogging } from "./Logger";
-import { setAppRootFolder, setExtensionContext } from "./utilities/extensionContext";
+import {
+ extensionContext,
+ setAppRootFolder,
+ setExtensionContext,
+} from "./utilities/extensionContext";
import { command } from "./utilities/subprocess";
import path from "path";
import os from "os";
import fs from "fs";
+import { SidepanelViewProvider } from "./panels/SidepanelViewProvider";
const BIN_MODIFICATION_DATE_KEY = "bin_modification_date";
+const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation";
function handleUncaughtErrors() {
process.on("unhandledRejection", (error) => {
@@ -47,6 +54,7 @@ export function deactivate(context: ExtensionContext): undefined {
export async function activate(context: ExtensionContext) {
handleUncaughtErrors();
+ IDEPanelLocationListener();
setExtensionContext(context);
if (context.extensionMode === ExtensionMode.Development) {
enableDevModeLogging();
@@ -54,14 +62,28 @@ export async function activate(context: ExtensionContext) {
await fixBinaries(context);
+ context.subscriptions.push(
+ window.registerWebviewViewProvider(
+ SidepanelViewProvider.viewType,
+ new SidepanelViewProvider(context)
+ )
+ );
context.subscriptions.push(
commands.registerCommand("RNIDE.openPanel", (fileName?: string, lineNumber?: number) => {
- PreviewsPanel.render(context, fileName, lineNumber);
+ if (workspace.getConfiguration("ReactNativeIDE").get("showPanelInActivityBar")) {
+ SidepanelViewProvider.showView(context, fileName, lineNumber);
+ } else {
+ Tabpanel.render(context, fileName, lineNumber);
+ }
})
);
context.subscriptions.push(
commands.registerCommand("RNIDE.showPanel", (fileName?: string, lineNumber?: number) => {
- PreviewsPanel.render(context, fileName, lineNumber);
+ if (workspace.getConfiguration("ReactNativeIDE").get("showPanelInActivityBar")) {
+ SidepanelViewProvider.showView(context, fileName, lineNumber);
+ } else {
+ Tabpanel.render(context, fileName, lineNumber);
+ }
})
);
context.subscriptions.push(
@@ -96,6 +118,17 @@ export async function activate(context: ExtensionContext) {
await configureAppRootFolder();
}
+function IDEPanelLocationListener() {
+ workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
+ if (!event.affectsConfiguration("ReactNativeIDE")) {
+ return;
+ }
+ if (workspace.getConfiguration("ReactNativeIDE").get("showPanelInActivityBar")) {
+ Tabpanel.currentPanel?.dispose();
+ }
+ });
+}
+
async function findSingleFileInWorkspace(fileGlobPattern: string, excludePattern: string | null) {
const files = await workspace.findFiles(fileGlobPattern, excludePattern, 2);
if (files.length === 1) {
@@ -112,13 +145,19 @@ function openWorkspaceSettings() {
});
}
+function extensionActivated() {
+ if (extensionContext.workspaceState.get(OPEN_PANEL_ON_ACTIVATION)) {
+ commands.executeCommand("RNIDE.openPanel");
+ }
+}
+
async function configureAppRootFolder() {
const appRootFolder = await findAppRootFolder();
if (appRootFolder) {
Logger.info(`Found app root folder: ${appRootFolder}`);
setAppRootFolder(appRootFolder);
commands.executeCommand("setContext", "RNIDE.extensionIsActive", true);
- PreviewsPanel.extensionActivated();
+ extensionActivated();
}
return appRootFolder;
}
diff --git a/packages/vscode-extension/src/panels/SidepanelViewProvider.ts b/packages/vscode-extension/src/panels/SidepanelViewProvider.ts
new file mode 100644
index 000000000..1a11f301b
--- /dev/null
+++ b/packages/vscode-extension/src/panels/SidepanelViewProvider.ts
@@ -0,0 +1,63 @@
+import { ExtensionContext, Uri, WebviewView, WebviewViewProvider, commands } from "vscode";
+import { generateWebviewContent } from "./webviewContentGenerator";
+import { extensionContext } from "../utilities/extensionContext";
+import { WebviewController } from "./WebviewController";
+import { Logger } from "../Logger";
+
+export class SidepanelViewProvider implements WebviewViewProvider {
+ public static readonly viewType = "ReactNativeIDE.view";
+ public static currentProvider: SidepanelViewProvider | undefined;
+ private _view: any = null;
+ private webviewController: any = null;
+
+ constructor(private readonly context: ExtensionContext) {
+ SidepanelViewProvider.currentProvider = this;
+ }
+
+ refresh(): void {
+ this._view.webview.html = generateWebviewContent(
+ this.context,
+ this._view.webview,
+ this.context.extensionUri
+ );
+ }
+
+ public static showView(context: ExtensionContext, fileName?: string, lineNumber?: number) {
+ if (SidepanelViewProvider.currentProvider) {
+ commands.executeCommand(`${SidepanelViewProvider.viewType}.focus`);
+ } else {
+ Logger.error("SidepanelViewProvider does not exist.");
+ return;
+ }
+
+ if (fileName !== undefined && lineNumber !== undefined) {
+ SidepanelViewProvider.currentProvider.webviewController.project.startPreview(
+ `preview:/${fileName}:${lineNumber}`
+ );
+ }
+ }
+
+ //called when a view first becomes visible
+ resolveWebviewView(webviewView: WebviewView): void | Thenable {
+ webviewView.webview.options = {
+ enableScripts: true,
+ localResourceRoots: [
+ Uri.joinPath(this.context.extensionUri, "dist"),
+ Uri.joinPath(this.context.extensionUri, "node_modules"),
+ ],
+ };
+ webviewView.webview.html = generateWebviewContent(
+ this.context,
+ webviewView.webview,
+ this.context.extensionUri
+ );
+ this._view = webviewView;
+ this.webviewController = new WebviewController(this._view.webview);
+ // Set an event listener to listen for when the webview is disposed (i.e. when the user changes
+ // settings or hiddes conteiner view by hand, https://code.visualstudio.com/api/references/vscode-api#WebviewView)
+ webviewView.onDidDispose(() => {
+ this.webviewController?.dispose();
+ });
+ commands.executeCommand("setContext", "RNIDE.previewsViewIsOpen", true);
+ }
+}
diff --git a/packages/vscode-extension/src/panels/Tabpanel.ts b/packages/vscode-extension/src/panels/Tabpanel.ts
new file mode 100644
index 000000000..45cb43456
--- /dev/null
+++ b/packages/vscode-extension/src/panels/Tabpanel.ts
@@ -0,0 +1,82 @@
+import { WebviewPanel, window, Uri, ViewColumn, ExtensionContext, commands } from "vscode";
+
+import { extensionContext } from "../utilities/extensionContext";
+import { generateWebviewContent } from "./webviewContentGenerator";
+import { WebviewController } from "./WebviewController";
+
+const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation";
+
+export class Tabpanel {
+ public static currentPanel: Tabpanel | undefined;
+ private readonly _panel: WebviewPanel;
+ private webviewController: WebviewController;
+
+ private constructor(panel: WebviewPanel) {
+ this._panel = panel;
+
+ // Set an event listener to listen for when the panel is disposed (i.e. when the user closes
+ // the panel or when the panel is closed programmatically)
+ this._panel.onDidDispose(() => this.dispose());
+
+ // Set the HTML content for the webview panel
+ this._panel.webview.html = generateWebviewContent(
+ extensionContext,
+ this._panel.webview,
+ extensionContext.extensionUri
+ );
+
+ this.webviewController = new WebviewController(this._panel.webview);
+ }
+
+ public static render(context: ExtensionContext, fileName?: string, lineNumber?: number) {
+ if (Tabpanel.currentPanel) {
+ // If the webview panel already exists reveal it
+ Tabpanel.currentPanel._panel.reveal(ViewColumn.Beside);
+ } else {
+ // If a webview panel does not already exist create and show a new one
+
+ // If there is an empty group in the editor, we will open the panel there:
+ const emptyGroup = window.tabGroups.all.find((group) => group.tabs.length === 0);
+
+ const panel = window.createWebviewPanel(
+ "react-native-ide-panel",
+ "React Native IDE",
+ { viewColumn: emptyGroup?.viewColumn || ViewColumn.Beside },
+ {
+ enableScripts: true,
+ localResourceRoots: [
+ Uri.joinPath(context.extensionUri, "dist"),
+ Uri.joinPath(context.extensionUri, "node_modules"),
+ ],
+ retainContextWhenHidden: true,
+ }
+ );
+ Tabpanel.currentPanel = new Tabpanel(panel);
+ context.workspaceState.update(OPEN_PANEL_ON_ACTIVATION, true);
+
+ commands.executeCommand("workbench.action.lockEditorGroup");
+ commands.executeCommand("setContext", "RNIDE.panelIsOpen", true);
+ }
+
+ if (fileName !== undefined && lineNumber !== undefined) {
+ Tabpanel.currentPanel.webviewController.project.startPreview(
+ `preview:/${fileName}:${lineNumber}`
+ );
+ }
+ }
+
+ public dispose() {
+ commands.executeCommand("setContext", "RNIDE.panelIsOpen", false);
+ // this is triggered when the user closes the webview panel by hand, we want to reset open_panel_on_activation
+ // key in this case to prevent extension from automatically opening the panel next time they open the editor
+ extensionContext.workspaceState.update(OPEN_PANEL_ON_ACTIVATION, undefined);
+
+ Tabpanel.currentPanel = undefined;
+
+ // Dispose of the current webview panel
+ this._panel.dispose();
+
+ //dispose of current webwiew dependencies
+ this.webviewController.dispose();
+ }
+}
diff --git a/packages/vscode-extension/src/panels/PreviewsPanel.ts b/packages/vscode-extension/src/panels/WebviewController.ts
similarity index 52%
rename from packages/vscode-extension/src/panels/PreviewsPanel.ts
rename to packages/vscode-extension/src/panels/WebviewController.ts
index ece6ed0c6..3892382f2 100644
--- a/packages/vscode-extension/src/panels/PreviewsPanel.ts
+++ b/packages/vscode-extension/src/panels/WebviewController.ts
@@ -1,60 +1,32 @@
-import {
- Disposable,
- Webview,
- WebviewPanel,
- window,
- Uri,
- ViewColumn,
- ExtensionContext,
- commands,
-} from "vscode";
-
-import { openExternalUrl } from "../utilities/vsc";
-import { extensionContext } from "../utilities/extensionContext";
-import { Logger } from "../Logger";
-import { generateWebviewContent } from "./webviewContentGenerator";
+import { Webview, Disposable, window } from "vscode";
import { DependencyChecker } from "../dependency/DependencyChecker";
import { DependencyInstaller } from "../dependency/DependencyInstaller";
import { DeviceManager } from "../devices/DeviceManager";
import { Project } from "../project/project";
+import { openExternalUrl } from "../utilities/vsc";
+import { Logger } from "../Logger";
+import { extensionContext } from "../utilities/extensionContext";
-const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation";
-
-export class PreviewsPanel {
- public static currentPanel: PreviewsPanel | undefined;
- private readonly _panel: WebviewPanel;
+export class WebviewController implements Disposable {
private readonly dependencyChecker: DependencyChecker;
private readonly dependencyInstaller: DependencyInstaller;
private readonly deviceManager: DeviceManager;
- private readonly project: Project;
+ public readonly project: Project;
private disposables: Disposable[] = [];
- private readonly callableObjects: Map;
-
private followEnabled = false;
- private constructor(panel: WebviewPanel) {
- this._panel = panel;
-
- // Set an event listener to listen for when the panel is disposed (i.e. when the user closes
- // the panel or when the panel is closed programmatically)
- this._panel.onDidDispose(() => this.dispose(), null, this.disposables);
-
- // Set the HTML content for the webview panel
- this._panel.webview.html = generateWebviewContent(
- extensionContext,
- this._panel.webview,
- extensionContext.extensionUri
- );
+ private readonly callableObjects: Map;
+ constructor(private webview: Webview) {
// Set an event listener to listen for messages passed from the webview context
- this._setWebviewMessageListener(this._panel.webview);
+ this._setWebviewMessageListener(webview);
// Set the manager to listen and change the persisting storage for the extension.
- this.dependencyChecker = new DependencyChecker(this._panel.webview);
+ this.dependencyChecker = new DependencyChecker(webview);
this.dependencyChecker.setWebviewMessageListener();
- this.dependencyInstaller = new DependencyInstaller(this._panel.webview);
+ this.dependencyInstaller = new DependencyInstaller(webview);
this.dependencyInstaller.setWebviewMessageListener();
this._setupEditorListeners();
@@ -75,59 +47,8 @@ export class PreviewsPanel {
]);
}
- public static extensionActivated() {
- if (extensionContext.workspaceState.get(OPEN_PANEL_ON_ACTIVATION)) {
- PreviewsPanel.render(extensionContext);
- }
- }
-
- public static render(context: ExtensionContext, fileName?: string, lineNumber?: number) {
- if (PreviewsPanel.currentPanel) {
- // If the webview panel already exists reveal it
- PreviewsPanel.currentPanel._panel.reveal(ViewColumn.Beside);
- } else {
- // If a webview panel does not already exist create and show a new one
-
- // If there is an empty group in the editor, we will open the panel there:
- const emptyGroup = window.tabGroups.all.find((group) => group.tabs.length === 0);
-
- const panel = window.createWebviewPanel(
- "react-native-ide-panel",
- "React Native IDE",
- { viewColumn: emptyGroup?.viewColumn || ViewColumn.Beside },
- {
- enableScripts: true,
- localResourceRoots: [
- Uri.joinPath(context.extensionUri, "dist"),
- Uri.joinPath(context.extensionUri, "node_modules"),
- ],
- retainContextWhenHidden: true,
- }
- );
- PreviewsPanel.currentPanel = new PreviewsPanel(panel);
- context.workspaceState.update(OPEN_PANEL_ON_ACTIVATION, true);
-
- commands.executeCommand("workbench.action.lockEditorGroup");
- commands.executeCommand("setContext", "RNIDE.panelIsOpen", true);
- }
-
- if (fileName !== undefined && lineNumber !== undefined) {
- PreviewsPanel.currentPanel.project.startPreview(`preview:/${fileName}:${lineNumber}`);
- }
- }
-
public dispose() {
- commands.executeCommand("setContext", "RNIDE.panelIsOpen", false);
- // this is triggered when the user closes the webview panel by hand, we want to reset open_panel_on_activation
- // key in this case to prevent extension from automatically opening the panel next time they open the editor
- extensionContext.workspaceState.update(OPEN_PANEL_ON_ACTIVATION, undefined);
-
- PreviewsPanel.currentPanel = undefined;
-
- // Dispose of the current webview panel
- this._panel.dispose();
-
- // Dispose of all disposables (i.e. commands) for the current webview panel
+ // Dispose of all disposables (i.e. commands) for the current webview
while (this.disposables.length) {
const disposable = this.disposables.pop();
if (disposable) {
@@ -136,6 +57,35 @@ export class PreviewsPanel {
}
}
+ private _setWebviewMessageListener(webview: Webview) {
+ webview.onDidReceiveMessage(
+ (message: any) => {
+ const command = message.command;
+
+ if (message.method !== "dispatchTouch") {
+ Logger.log("Message from webview", message);
+ }
+
+ switch (command) {
+ case "call":
+ this.handleRemoteCall(message);
+ return;
+ case "openExternalUrl":
+ openExternalUrl(message.url);
+ return;
+ case "stopFollowing":
+ this.followEnabled = false;
+ return;
+ case "startFollowing":
+ this.followEnabled = true;
+ return;
+ }
+ },
+ undefined,
+ this.disposables
+ );
+ }
+
private handleRemoteCall(message: any) {
const { object, method, args, callId } = message;
const callableObject = this.callableObjects.get(object);
@@ -144,7 +94,7 @@ export class PreviewsPanel {
if (typeof arg === "object" && "__callbackId" in arg) {
const callbackId = arg.__callbackId;
return (...args: any[]) => {
- this._panel.webview.postMessage({
+ this.webview.postMessage({
command: "callback",
callbackId,
args,
@@ -159,21 +109,21 @@ export class PreviewsPanel {
if (result instanceof Promise) {
result
.then((result) => {
- this._panel.webview.postMessage({
+ this.webview.postMessage({
command: "callResult",
callId,
result,
});
})
.catch((error) => {
- this._panel.webview.postMessage({
+ this.webview.postMessage({
command: "callResult",
callId,
error,
});
});
} else {
- this._panel.webview.postMessage({
+ this.webview.postMessage({
command: "callResult",
callId,
result,
@@ -182,35 +132,6 @@ export class PreviewsPanel {
}
}
- private _setWebviewMessageListener(webview: Webview) {
- webview.onDidReceiveMessage(
- (message: any) => {
- const command = message.command;
-
- if (message.method !== "dispatchTouch") {
- Logger.log("Message from webview", message);
- }
-
- switch (command) {
- case "call":
- this.handleRemoteCall(message);
- return;
- case "openExternalUrl":
- openExternalUrl(message.url);
- return;
- case "stopFollowing":
- this.followEnabled = false;
- return;
- case "startFollowing":
- this.followEnabled = true;
- return;
- }
- },
- undefined,
- this.disposables
- );
- }
-
private _setupEditorListeners() {
extensionContext.subscriptions.push(
window.onDidChangeActiveTextEditor((editor) => {