Skip to content

Commit

Permalink
refactor(webview): restructure and re-organize code, add jsdocs
Browse files Browse the repository at this point in the history
Signed-off-by: Konstantinos Maninakis <maninak@protonmail.com>
  • Loading branch information
maninak committed Dec 13, 2023
1 parent 0952def commit a482022
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 105 deletions.
74 changes: 12 additions & 62 deletions lib/webview-messaging.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,22 @@
import type { Webview } from 'vscode'
import { getVscodeRef } from '../src/webviews/src/utils/getVscodeRef'

let vscode: ReturnType<typeof acquireVsCodeApi> | undefined

export function getVscodeRef() {
if (vscode) {
return vscode
}

return (vscode = acquireVsCodeApi())
interface Message<Command extends string, Payload extends object | undefined = undefined> {
command: Command
payload: Payload
}

export interface MessageToWebview {
command: 'resetCount'
}
type MessageToWebview = Message<'resetCount'>

export function postMessageToWebview(message: MessageToWebview, webview: Webview): void {
webview.postMessage(message)
}
type MessageToExtension = Message<'showInfoNotification', { text: string }>
// append more message types with `| Message<'...', SomeType>`

export interface MessageToExtension {
command: 'showInfoNotification'
text: string
/** Sends a message, usually from the host window, to the provided webview. */
export function notifyWebview(message: MessageToWebview, webview: Webview): void {
webview.postMessage(message)
}

export function postMessageToExtension(message: MessageToExtension): void {
/** Sends a message from within a webview to the VS Code extension hosting it. */
export function notifyExtension(message: MessageToExtension): void {
getVscodeRef().postMessage(message)
}

// Source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/vscode-webview/index.d.ts
/**
* API exposed to webviews.
*
* @template StateType Type of the persisted state stored for the webview.
*/
export interface WebviewApi<StateType> {
/**
* Post a message to the owner of the webview.
*
* @param message Data to post. Must be JSON serializable.
*/
postMessage(message: unknown): void

/**
* Get the persistent state stored for this webview.
*
* @return The current state or `undefined` if no state has been set.
*/
getState(): StateType | undefined

/**
* Set the persistent state stored for this webview.
*
* @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
* using {@link getState}.
*
* @return The new state.
*/
setState<T extends StateType | undefined>(newState: T): T
}

declare global {
/**
* Acquire an instance of the webview API.
*
* This may only be called once in a webview's context. Attempting to call `acquireVsCodeApi` after it has already
* been called will throw an exception.
*
* @template StateType Type of the persisted state stored for the webview.
*/
function acquireVsCodeApi<StateType = unknown>(): WebviewApi<StateType>
}
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@
"contributes": {
"commands": [
{
"command": "radicle.test-webview",
"title": "Open dat webview",
"category": "Radicle"
"command": "wip-webview",
"title": "Open dat wip webview"
},
{
"command": "radicle.sync",
Expand Down
4 changes: 2 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export function activate(ctx: ExtensionContext) {
validateRadicleIdentityAuthentication({ minimizeUserNotifications: true })
validateHttpdConnection({ minimizeUserNotifications: true })

// TODO: maninak remove or obfuscate?
// TODO: delete registration code from here and package.json when done with prototyping
ctx.subscriptions.push(
commands.registerCommand('radicle.test-webview', () => {
commands.registerCommand('wip-webview', () => {
createOrShowWebview(ctx)
}),
)
Expand Down
34 changes: 23 additions & 11 deletions src/helpers/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,36 @@ import {
type ExtensionContext,
Uri,
ViewColumn,
type Webview,
type WebviewOptions,
type WebviewPanel,
window,
} from 'vscode'
import { getExtensionContext } from '../store'
import { assertUnreachable, getNonce, getUri } from '../utils'
import { assertUnreachable, getNonce } from '../utils'
import {
type MessageToExtension,
type MessageToWebview,
postMessageToWebview,
type notifyExtension,
notifyWebview as notifyWebviewBase,
} from '../../lib/webview-messaging'

export const webviewId = 'webview-patch-detail'

let panel: WebviewPanel | undefined

// TODO: maninak move this (and other files from helpers to "/services" or "/providers")
export function notifyWebview(message: Parameters<typeof notifyWebviewBase>['0']): void {
panel && notifyWebviewBase(message, panel.webview)
}

// TODO: maninak move this file (and other found in helpers) to "/services" or "/providers"

// TODO: maninak document
/**
* Opens a panel with the specified webview in the active column.
*
* If the webview is already open and visible in another column it will be moved to the active
* column without getting re-created.
*
* @param [title] - The title shown on the webview panel's tab
*/
export function createOrShowWebview(ctx: ExtensionContext, title = 'Patch DEADBEEF') {
const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : undefined

Expand Down Expand Up @@ -90,12 +101,13 @@ export function createOrShowWebview(ctx: ExtensionContext, title = 'Patch DEADBE
// panel.onDidChangeViewState()

panel.webview.onDidReceiveMessage(
(message: MessageToExtension) => {
(message: Parameters<typeof notifyExtension>['0']) => {
switch (message.command) {
case 'showInfoNotification': {
const button = 'Reset Count'
window.showInformationMessage(message.text, button).then((userSelection) => {
userSelection === button && postMsgToWebview({ command: 'resetCount' })
window.showInformationMessage(message.payload.text, button).then((userSelection) => {
userSelection === button &&
notifyWebview({ command: 'resetCount', payload: undefined })
})
break
}
Expand All @@ -110,6 +122,6 @@ export function createOrShowWebview(ctx: ExtensionContext, title = 'Patch DEADBE
panel.onDidDispose(() => (panel = undefined), undefined, ctx.subscriptions)
}

export function postMsgToWebview(message: MessageToWebview): void {
panel && postMessageToWebview(message, panel.webview)
function getUri(webview: Webview, extensionUri: Uri, pathList: string[]): Uri {
return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList))
}
5 changes: 4 additions & 1 deletion src/utils/getNonce.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// TODO: maninak document
/**
* Returns a pseudorandom string to be used only once
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
*/
export function getNonce(): string {
let text = ''
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
Expand Down
6 changes: 0 additions & 6 deletions src/utils/getUri.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from './askUser'
export * from './assertUnreachable'
export * from './exec'
export * from './getNonce'
export * from './getUri'
export * from './git'
export * from './log'
export * from './memoizeWithDebouncedCacheClear'
Expand Down
19 changes: 3 additions & 16 deletions src/webviews/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
<script setup lang="ts">
import Counter from './components/Counter.vue'
import { useEventListener } from '@vueuse/core'
import { useCounterStore } from '@/stores/counter'
import type { MessageToWebview } from 'lib/webview-messaging'
import { assertUnreachable } from 'utils/assertUnreachable'
import Counter from '@/components/Counter.vue'
import { registerWebviewMessageHandlers } from '@/composables/registerWebviewMessageHandlers'
// TODO: maninak add linter and hook-up scripts
// TODO: maninak add component auto-import
// TODO: maninak add auto-import vue-use
useEventListener(window, 'message', (event: MessageEvent<MessageToWebview>) => {
const message = event.data
switch (message.command) {
case 'resetCount':
useCounterStore().reset()
break
default:
assertUnreachable(message.command)
}
})
registerWebviewMessageHandlers()
</script>

<template>
Expand Down
6 changes: 3 additions & 3 deletions src/webviews/src/components/Counter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit'
import { postMessageToExtension } from 'lib/webview-messaging'
import { notifyExtension } from 'lib/webview-messaging'
provideVSCodeDesignSystem().register(vsCodeButton())
Expand All @@ -12,9 +12,9 @@ const { count, doubleCount } = storeToRefs(counterStore)
const { increment, reset } = counterStore
function showInfoNotifWithCount() {
postMessageToExtension({
notifyExtension({
command: "showInfoNotification",
text: `The count is: ${count.value}`,
payload: { text: `The count is: ${count.value}` },
})
}
</script>
Expand Down
22 changes: 22 additions & 0 deletions src/webviews/src/composables/registerWebviewMessageHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

import { useEventListener } from '@vueuse/core'
import { useCounterStore } from '@/stores/counter'
import type { notifyWebview } from 'lib/webview-messaging'
import { assertUnreachable } from 'utils/assertUnreachable'

/**
* Registers a handler for each possible message the extension can post to the webview.
*/
export function registerWebviewMessageHandlers(){
useEventListener(window, 'message', (event: MessageEvent<Parameters<typeof notifyWebview>['0']>) => {
const message = event.data

switch (message.command) {
case 'resetCount':
useCounterStore().reset()
break
default:
assertUnreachable(message.command)
}
})
}
57 changes: 57 additions & 0 deletions src/webviews/src/utils/getVscodeRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

let vscode: ReturnType<typeof acquireVsCodeApi> | undefined

/** Resolves a reference to the VS Code context auto-injected in a webview. */
export function getVscodeRef() {
if (vscode) {
return vscode
}

return (vscode = acquireVsCodeApi())
}

// Typings copied from
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/vscode-webview/index.d.ts

/**
* API exposed to webviews.
*
* @template StateType Type of the persisted state stored for the webview.
*/
export interface WebviewApi<StateType> {
/**
* Post a message to the owner of the webview.
*
* @param message Data to post. Must be JSON serializable.
*/
postMessage(message: unknown): void

/**
* Get the persistent state stored for this webview.
*
* @return The current state or `undefined` if no state has been set.
*/
getState(): StateType | undefined

/**
* Set the persistent state stored for this webview.
*
* @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
* using {@link getState}.
*
* @return The new state.
*/
setState<T extends StateType | undefined>(newState: T): T
}

declare global {
/**
* Acquire an instance of the webview API.
*
* This may only be called once in a webview's context. Attempting to call `acquireVsCodeApi` after it has already
* been called will throw an exception.
*
* @template StateType Type of the persisted state stored for the webview.
*/
function acquireVsCodeApi<StateType = unknown>(): WebviewApi<StateType>
}

0 comments on commit a482022

Please sign in to comment.