major refactor, allow 3 types of webview

This commit is contained in:
Andrew 2024-10-25 19:49:59 -07:00
parent a51873c06a
commit 38662d2824
30 changed files with 224 additions and 253 deletions

View file

@ -7,7 +7,7 @@ There are two main ways to contribute:
- Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
Void's functionality is primarily implemented using a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension). Continue reading to learn how to 1. build/contribute to the Extension, or 2. build/contribute to the full IDE (for more extensive changes).
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).

View file

@ -1,19 +0,0 @@
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const fs = require('fs')
const from = 'src/sidebar/styles.css'
const to = 'dist/sidebar/styles.css'
const original_css_contents = fs.readFileSync(from, 'utf8')
postcss([
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
autoprefixer,
])
.process(original_css_contents, { from, to })
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
.catch(error => {
console.error('Error in build-css:', error)
})

View file

@ -1,13 +0,0 @@
const esbuild = require('esbuild')
// Build JS
esbuild.build({
entryPoints: ['src/sidebar/index.tsx'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/sidebar/index.js',
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
}).catch(() => process.exit(1));

View file

@ -0,0 +1,59 @@
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const fs = require('fs')
const convertTailwindToCSS = ({ from, to }) => {
console.log('converting ', from, ' --> ', to)
const original_css_contents = fs.readFileSync(from, 'utf8')
return postcss([
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
autoprefixer,
])
.process(original_css_contents, { from, to })
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
.catch(error => {
console.error('Error in build-css:', error)
})
}
const esbuild = require('esbuild')
const convertTSXtoJS = async ({ from, to }) => {
console.log('converting ', from, ' --> ', to)
return esbuild.build({
entryPoints: [from],
bundle: true,
minify: true,
sourcemap: true,
outfile: to,
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
}).catch(() => process.exit(1));
}
(async () => {
// convert tsx to js
await convertTSXtoJS({
from: 'src/webviews/sidebar/index.tsx',
to: 'dist/webviews/sidebar/index.js',
})
await convertTSXtoJS({
from: 'src/webviews/ctrlk/index.tsx',
to: 'dist/webviews/ctrlk/index.js',
})
// convert tailwind to css
await convertTailwindToCSS({
from: 'src/webviews/styles.css',
to: 'dist/webviews/styles.css',
})
})()

View file

@ -104,7 +104,7 @@
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"build": "rimraf dist && node build-tsx.js && node build-css.js",
"build": "rimraf dist && node build/build.js",
"pretest": "tsc -p ./ && eslint src --ext ts",
"test": "vscode-test"
},

View file

@ -1,7 +1,7 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { VoidConfig } from '../sidebar/contextForConfig';
import { VoidConfig } from '../webviews/common/contextForConfig'

View file

@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { PartialVoidConfig } from '../sidebar/contextForConfig';
import { PartialVoidConfig } from '../webviews/common/contextForConfig'

View file

@ -1,77 +0,0 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
// private _webviewView?: vscode.WebviewView;
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res })
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
this._res = temp_res
}
// called by us
updateWebviewHTML(webview: vscode.Webview) {
this._webviewDeps = []
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
const nonce = generateNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<div id="ctrlkroot"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML;
}
// called internally by vscode
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken,
) {
const webview = webviewView.webview;
webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
this.updateWebviewHTML(webview);
// resolve webview and _webviewView
this._res(webview);
// this._webviewView = webviewView;
}
}

View file

@ -1,85 +0,0 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
function generateNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
// private _webviewView?: vscode.WebviewView;
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res })
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
this._res = temp_res
}
// called by us
updateWebviewHTML(webview: vscode.Webview) {
this._webviewDeps = []
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
const nonce = generateNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<div id="ctrlkroot"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML;
}
// called internally by vscode
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken,
) {
const webview = webviewView.webview;
webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
this.updateWebviewHTML(webview);
// resolve webview and _webviewView
this._res(webview);
// this._webviewView = webviewView;
}
}

View file

@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from '../common/shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { v4 as uuidv4 } from 'uuid'
// this comes from vscode.proposed.editorInsets.d.ts

View file

@ -0,0 +1,47 @@
import * as vscode from 'vscode'
function generateNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
// call this when you have access to the webview to set its html
export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.Uri, { jsLocation, cssLocation }: { jsLocation: string, cssLocation: string }) => {
// 'dist/sidebar/index.js'
// 'dist/sidebar/styles.css'
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, jsLocation));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, cssLocation));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri));
const nonce = generateNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<div id="ctrlkroot"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML
webview.options = {
enableScripts: true,
localResourceRoots: [extensionUri]
};
}

View file

@ -0,0 +1,21 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
export class CtrlKWebviewProvider {
private readonly _extensionUri: vscode.Uri
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
}
// called by us
updateWebviewHTML(webview: vscode.Webview) {
_updateWebviewHTML(webview, this._extensionUri, { jsLocation: 'dist/webviews/ctrlk/index.js', cssLocation: 'dist/webviews/styles.css' })
}
}

View file

@ -0,0 +1,35 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res })
if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
this._res = temp_res
}
// called by us
updateWebviewHTML(webview: vscode.Webview) {
_updateWebviewHTML(webview, this._extensionUri, { jsLocation: 'dist/webviews/sidebar/index.js', cssLocation: 'dist/webviews/styles.css' })
}
// called internally by vscode
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken,) {
const webview = webviewView.webview;
this.updateWebviewHTML(webview);
this._res(webview); // resolve webview and _webviewView
}
}

View file

@ -1,10 +0,0 @@
function generateNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View file

@ -1,5 +1,5 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../common/shared_types"
import { ChatMessage, ChatThreads } from "../../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { MessageFromSidebar, MessageToSidebar, } from "../common/shared_types";
import { MessageFromSidebar, MessageToSidebar, } from "../../common/shared_types";
import { v4 as uuidv4 } from 'uuid';

View file

@ -1,20 +1,12 @@
import * as React from "react"
import { useEffect } from "react"
import React, { useEffect } from "react";
import * as ReactDOM from "react-dom/client"
import Sidebar from "./Sidebar"
import { CtrlK } from "./CtrlK"
import { ThreadsProvider } from "./contextForThreads"
import { ConfigProvider } from "./contextForConfig"
import { MessageToSidebar } from "../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi"
import { identifyUser, initPosthog } from "./metrics/posthog"
import { MessageToSidebar } from "../../common/shared_types";
import { getVSCodeAPI, awaitVSCodeResponse, onMessageFromVSCode } from "./getVscodeApi";
import { initPosthog, identifyUser } from "./posthog";
import { ThreadsProvider } from "./contextForThreads";
import { ConfigProvider } from "./contextForConfig";
if (typeof document === "undefined") {
console.log("index.tsx error: document was undefined")
}
const CommonEffects = () => {
const ListenersAndTracking = () => {
// initialize posthog
useEffect(() => {
initPosthog()
@ -41,26 +33,31 @@ const CommonEffects = () => {
return null
}
(() => {
export const mount = (children: React.ReactNode) => {
if (typeof document === "undefined") {
console.error("index.tsx error: document was undefined")
return
}
// mount the sidebar on the id="root" element
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const sidebar = (<>
<CommonEffects />
const content = (<>
<ListenersAndTracking />
<ThreadsProvider>
<ConfigProvider>
<Sidebar />
{children}
</ConfigProvider>
</ThreadsProvider>
<ConfigProvider>
<CtrlK />
</ConfigProvider>
</>)
const root = ReactDOM.createRoot(rootElement)
root.render(sidebar)
})();
const root = ReactDOM.createRoot(rootElement)
root.render(content);
}

View file

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from './getVscodeApi';
import { useOnVSCodeMessage } from '../common/getVscodeApi';
export const CtrlK = () => {

View file

@ -0,0 +1,7 @@
import React from "react"
import { mount } from "../common/mount"
import { CtrlK } from "./CtrlK"
// this is the entry point that mounts ctrlk
mount(<CtrlK />)

View file

@ -1,11 +1,11 @@
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { CodeSelection, ChatMessage, MessageToSidebar } from "../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"
import { CodeSelection, ChatMessage, MessageToSidebar } from "../../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi"
import { SidebarThreadSelector } from "./SidebarThreadSelector";
import { SidebarChat } from "./SidebarChat";
import { SidebarSettings } from './SidebarSettings';
import { identifyUser } from "./metrics/posthog";
import { SidebarSettings } from "./SidebarSettings";
import { identifyUser } from "../common/posthog";
const Sidebar = () => {

View file

@ -4,13 +4,13 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from "reac
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import { File, ChatMessage, CodeSelection } from "../common/shared_types";
import { File, ChatMessage, CodeSelection } from "../../common/shared_types";
import * as vscode from 'vscode'
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
import { useThreads } from "./contextForThreads";
import { sendLLMMessage } from "../common/sendLLMMessage";
import { useVoidConfig } from "./contextForConfig";
import { captureEvent } from "./metrics/posthog";
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi";
import { useThreads } from "../common/contextForThreads";
import { sendLLMMessage } from "../../common/sendLLMMessage";
import { useVoidConfig } from "../common/contextForConfig";
import { captureEvent } from "../common/posthog";

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig";
import { configFields, useVoidConfig, VoidConfigField } from "../common/contextForConfig";
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {

View file

@ -1,5 +1,5 @@
import React from "react";
import { ThreadsProvider, useThreads } from "./contextForThreads";
import { ThreadsProvider, useThreads } from "../common/contextForThreads";
const truncate = (s: string) => {

View file

@ -0,0 +1,7 @@
import React from "react"
import Sidebar from "./Sidebar"
import { mount } from "../common/mount"
// this is the entry point that mounts the sidebar
mount(<Sidebar />)

View file

@ -1,7 +1,7 @@
import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
import { getVSCodeAPI } from "../getVscodeApi"
import { getVSCodeAPI } from "../../common/getVscodeApi"
enum CopyButtonState {

View file

@ -1,3 +1,5 @@
/* all the styles are shared right now between all webviews */
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -2,7 +2,7 @@
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
module.exports = {
content: ["./src/sidebar/**/*.{html,js,ts,jsx,tsx}"],
content: ["./src/webviews/**/*.{html,js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {