diff --git a/package.json b/package.json index cf6c694..82c3a46 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,46 @@ "activationEvents": [], "main": "./dist/extension.js", "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "custom-activitybar", + "title": "VSCode Extension", + "icon": "assets/logo_bito.svg" + } + ] + }, + "views": { + "custom-activitybar": [ + { + "type": "webview", + "id": "vscodeSidebar.openview", + "name": "View", + "contextualTitle": "View" + } + ] + }, "commands": [ { - "command": "claude-dev.helloWorld", - "title": "Hello World" + "command": "vscodeSidebar.openview", + "title": "Sidebar View" + }, + { + "command": "vscodeSidebar.menu.view", + "category": "vscode-extension-sidebar-html", + "title": "Sample WebView in VS Code Sidebar", + "icon": "$(clear-all)" } - ] + ], + "menus": { + "view/title": [ + { + "command": "vscodeSidebar.menu.view", + "group": "navigation", + "when": "view == vscodeSidebar.openview" + } + ] + } }, "scripts": { "vscode:prepublish": "npm run package", diff --git a/src/assets/icon.png b/src/assets/icon.png new file mode 100644 index 0000000..a6cde3a Binary files /dev/null and b/src/assets/icon.png differ diff --git a/src/extension.ts b/src/extension.ts index c004365..585b5df 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,16 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode" import { HelloWorldPanel } from "./HelloWorldPanel" +import { SidebarProvider } from "./providers/SidebarProvider" + +/* +Built using https://github.com/microsoft/vscode-webview-ui-toolkit + +Inspired by +https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview +https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra + +*/ // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -21,11 +31,32 @@ export function activate(context: vscode.ExtensionContext) { // context.subscriptions.push(disposable) - const helloCommand = vscode.commands.registerCommand("claude-dev.helloWorld", () => { - HelloWorldPanel.render(context.extensionUri) + // const helloCommand = vscode.commands.registerCommand("claude-dev.helloWorld", () => { + // HelloWorldPanel.render(context.extensionUri) + // }) + + // context.subscriptions.push(helloCommand) + + const provider = new SidebarProvider(context.extensionUri) + + context.subscriptions.push(vscode.window.registerWebviewViewProvider(SidebarProvider.viewType, provider)) + + context.subscriptions.push( + vscode.commands.registerCommand("vscodeSidebar.menu.view", () => { + const message = "Menu/Title of extension is clicked !" + vscode.window.showInformationMessage(message) + }) + ) + + // Command has been defined in the package.json file + // Provide the implementation of the command with registerCommand + // CommandId parameter must match the command field in package.json + let openWebView = vscode.commands.registerCommand("vscodeSidebar.openview", () => { + // Display a message box to the user + vscode.window.showInformationMessage('Command " Sidebar View [vscodeSidebar.openview] " called.') }) - context.subscriptions.push(helloCommand) + context.subscriptions.push(openWebView) } // This method is called when your extension is deactivated diff --git a/src/providers/SidebarProvider.ts b/src/providers/SidebarProvider.ts new file mode 100644 index 0000000..c16a5d2 --- /dev/null +++ b/src/providers/SidebarProvider.ts @@ -0,0 +1,78 @@ +import { getUri } from "../utilities/getUri" +import { getNonce } from "../utilities/getNonce" +//import * as weather from "weather-js" +import * as vscode from "vscode" + +export class SidebarProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "vscodeSidebar.openview" + + private _view?: vscode.WebviewView + + constructor(private readonly _extensionUri: vscode.Uri) {} + + resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + token: vscode.CancellationToken + ): void | Thenable { + this._view = webviewView + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + localResourceRoots: [this._extensionUri], + } + webviewView.webview.html = this.getHtmlContent(webviewView.webview) + } + + private getHtmlContent(webview: vscode.Webview): string { + // Get the local path to main script run in the webview, + // then convert it to a uri we can use in the webview. + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js")) + + const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css")) + const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css")) + + // Same for stylesheet + const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css")) + + // Use a nonce to only allow a specific script to be run. + const nonce = getNonce() + + return ` + + + + + + + + + + + + + + + + +
+
+
+

Subscribe today

+ + + + +

We won’t send you spam.

+

Unsubscribe at any time.

+ +
+
+
+ + + + ` + } +} diff --git a/src/utilities/getNonce.ts b/src/utilities/getNonce.ts index 63eecfd..b92871b 100644 --- a/src/utilities/getNonce.ts +++ b/src/utilities/getNonce.ts @@ -1,3 +1,11 @@ +/** + * A helper function that returns a unique alphanumeric identifier called a nonce. + * + * @remarks This function is primarily used to help enforce content security + * policies for resources/scripts being executed in a webview context. + * + * @returns A nonce + */ export function getNonce() { let text = "" const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" diff --git a/src/utilities/getUri.ts b/src/utilities/getUri.ts index 9a2a5b3..b9f6807 100644 --- a/src/utilities/getUri.ts +++ b/src/utilities/getUri.ts @@ -1,5 +1,16 @@ import { Uri, Webview } from "vscode" +/** + * A helper function which will get the webview URI of a given file or resource. + * + * @remarks This URI can be used within a webview's HTML as a link to the + * given file/resource. + * + * @param webview A reference to the extension webview + * @param extensionUri The URI of the directory containing the extension + * @param pathList An array of strings representing the path to a file/resource + * @returns A URI pointing to the file/resource + */ export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)) } diff --git a/src/webview/main.ts b/src/webview/main.ts index 7244f8a..56a7cca 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -1,14 +1,8 @@ -import { provideVSCodeDesignSystem, vsCodeButton, vsCodeCheckbox } from "@vscode/webview-ui-toolkit" -// const toolkit = require("@vscode/webview-ui-toolkit") -// /* -// You must register the components you want to use -// */ + +/* provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeCheckbox()) - const vscode = acquireVsCodeApi(); - window.addEventListener("load", main); - function main() { // To get improved type annotations/IntelliSense the associated class for // a given toolkit component can be imported and used to type cast a reference @@ -16,10 +10,165 @@ function main() { const howdyButton = document.getElementById("howdy") as Button; howdyButton?.addEventListener("click", handleHowdyClick); } - function handleHowdyClick() { vscode.postMessage({ command: "hello", text: "Hey there partner! 🀠", }); -} \ No newline at end of file +} + */ + + +import { + provideVSCodeDesignSystem, + Button, + Dropdown, + ProgressRing, + TextField, + vsCodeButton, + vsCodeDropdown, + vsCodeOption, + vsCodeTextField, + vsCodeProgressRing, + } from "@vscode/webview-ui-toolkit"; + + // In order to use the Webview UI Toolkit web components they + // must be registered with the browser (i.e. webview) using the + // syntax below. + provideVSCodeDesignSystem().register( + vsCodeButton(), + vsCodeDropdown(), + vsCodeOption(), + vsCodeProgressRing(), + vsCodeTextField() + ); + + // Get access to the VS Code API from within the webview context + const vscode = acquireVsCodeApi(); + + // Just like a regular webpage we need to wait for the webview + // DOM to load before we can reference any of the HTML elements + // or toolkit components + window.addEventListener("load", main); + + // Main function that gets executed once the webview DOM loads + function main() { + // To get improved type annotations/IntelliSense the associated class for + // a given toolkit component can be imported and used to type cast a reference + // to the element (i.e. the `as Button` syntax) + const checkWeatherButton = document.getElementById("check-weather-button") as Button; + checkWeatherButton.addEventListener("click", checkWeather); + + setVSCodeMessageListener(); + } + + function checkWeather() { + const location = document.getElementById("location") as TextField; + const unit = document.getElementById("unit") as Dropdown; + + // Passes a message back to the extension context with the location that + // should be searched for and the degree unit (F or C) that should be returned + vscode.postMessage({ + command: "weather", + location: location.value, + unit: unit.value, + }); + + displayLoadingState(); + } + + // Sets up an event listener to listen for messages passed from the extension context + // and executes code based on the message that is recieved + function setVSCodeMessageListener() { + window.addEventListener("message", (event) => { + const command = event.data.command; + + // switch (command) { + // case "weather": + // const weatherData = JSON.parse(event.data.payload); + // displayWeatherData(weatherData); + // break; + // case "error": + // displayError(event.data.message); + // break; + // } + }); + } + + function displayLoadingState() { + const loading = document.getElementById("loading") as ProgressRing; + const icon = document.getElementById("icon"); + const summary = document.getElementById("summary"); + if (loading && icon && summary) { + loading.classList.remove("hidden"); + icon.classList.add("hidden"); + summary.textContent = "Getting weather..."; + } + } + +// function displayWeatherData(weatherData) { +// const loading = document.getElementById("loading") as ProgressRing; +// const icon = document.getElementById("icon"); +// const summary = document.getElementById("summary"); +// if (loading && icon && summary) { +// loading.classList.add("hidden"); +// icon.classList.remove("hidden"); +// icon.textContent = getWeatherIcon(weatherData); +// summary.textContent = getWeatherSummary(weatherData); +// } +// } + +// function displayError(errorMsg) { +// const loading = document.getElementById("loading") as ProgressRing; +// const icon = document.getElementById("icon"); +// const summary = document.getElementById("summary"); +// if (loading && icon && summary) { +// loading.classList.add("hidden"); +// icon.classList.add("hidden"); +// summary.textContent = errorMsg; +// } +// } + +// function getWeatherSummary(weatherData) { +// const skyText = weatherData.current.skytext; +// const temperature = weatherData.current.temperature; +// const degreeType = weatherData.location.degreetype; + +// return `${skyText}, ${temperature}${degreeType}`; +// } + +// function getWeatherIcon(weatherData) { +// const skyText = weatherData.current.skytext.toLowerCase(); +// let icon = ""; + +// switch (skyText) { +// case "sunny": +// icon = "β˜€οΈ"; +// break; +// case "mostly sunny": +// icon = "🌀"; +// break; +// case "partly sunny": +// icon = "πŸŒ₯"; +// break; +// case "clear": +// icon = "β˜€οΈ"; +// break; +// case "fair": +// icon = "πŸŒ₯"; +// break; +// case "mostly cloudy": +// icon = "☁️"; +// break; +// case "cloudy": +// icon = "☁️"; +// break; +// case "rain showers": +// icon = "🌦"; +// break; +// default: +// icon = "✨"; +// } + +// return icon; +// } \ No newline at end of file diff --git a/src/webview/styles.css b/src/webview/styles.css new file mode 100644 index 0000000..4cb0dd7 --- /dev/null +++ b/src/webview/styles.css @@ -0,0 +1,45 @@ +h1 { + font-size: 1.5em; +} + +#search-container { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} + +#location { + width: 100%; + margin-top: 0.5rem; +} + +#unit { + min-width: 30px; + width: 100%; + margin-top: 0.5rem; +} + +#check-weather-button { + margin-top: 0.5rem; +} + +#results-container { + display: flex; + align-items: center; + justify-content: space-around; + background-color: var(--vscode-input-background); + padding: 1rem; + margin: 1rem 0; + border-radius: 2px; +} + +#icon { + font-size: 3em; + padding: 0; + margin: 0; +} + +.hidden { + display: none; +}