Skip to content

Commit

Permalink
Preview in Activity Bar (#22)
Browse files Browse the repository at this point in the history
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

<img width="535" alt="Screenshot 2024-03-18 at 21 49 03"
src="https://github.com/software-mansion-labs/react-native-sztudio/assets/159789821/abef7f75-8ae7-4d97-827a-6b962a53dbda">
<img width="728" alt="Screenshot 2024-03-18 at 21 49 58"
src="https://github.com/software-mansion-labs/react-native-sztudio/assets/159789821/803684dd-6163-4920-a1f1-8123debb9309">

---------

Co-authored-by: Filip Andrzej Kaminski <[email protected]>
  • Loading branch information
filip131311 and Filip Andrzej Kaminski authored Mar 26, 2024
1 parent caa8b3d commit c03ef64
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 130 deletions.
3 changes: 3 additions & 0 deletions packages/vscode-extension/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 26 additions & 1 deletion packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
},
Expand Down
49 changes: 44 additions & 5 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -47,21 +54,36 @@ export function deactivate(context: ExtensionContext): undefined {

export async function activate(context: ExtensionContext) {
handleUncaughtErrors();
IDEPanelLocationListener();
setExtensionContext(context);
if (context.extensionMode === ExtensionMode.Development) {
enableDevModeLogging();
}

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<boolean>("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<boolean>("showPanelInActivityBar")) {
SidepanelViewProvider.showView(context, fileName, lineNumber);
} else {
Tabpanel.render(context, fileName, lineNumber);
}
})
);
context.subscriptions.push(
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
63 changes: 63 additions & 0 deletions packages/vscode-extension/src/panels/SidepanelViewProvider.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
82 changes: 82 additions & 0 deletions packages/vscode-extension/src/panels/Tabpanel.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit c03ef64

Please sign in to comment.