Compare commits

...

24 Commits

Author SHA1 Message Date
mp 5c07136b36 Minor edit 2024-11-01 16:30:36 -07:00
mp 580f9b6e02 Implement basic Inset functionality 2024-11-01 16:18:34 -07:00
Andrew 68bc7ba620 update contrib 2024-10-31 21:58:02 -07:00
mp dd48db0ed3 minor changes 2024-10-31 19:23:17 -07:00
mp d7c00addd5 update inline diffs 2024-10-31 17:10:39 -07:00
Andrew Pareles b34119c4df Merge pull request #134 from Piyu-Pika/Gemini
Gemini added #new feature
2024-10-30 23:19:52 -07:00
Andrew Pareles d098291c90 minor note 2024-10-30 23:19:34 -07:00
Andrew ed9e126a4a remove duplicate extension.ts 2024-10-30 23:16:36 -07:00
Andrew a8010eec15 simplify gemini 2024-10-30 22:46:47 -07:00
Piyu-Pika 8cd1ea305c Gemini 2024-10-31 10:41:04 +05:30
Piyush Bhardwaj 762264b030 Merge branch 'main' into Gemini 2024-10-31 10:28:17 +05:30
Andrew Pareles 48d5369196 amend 2024-10-30 19:09:43 -07:00
Andrew Pareles aab840db2d amend 2024-10-30 19:02:48 -07:00
Andrew Pareles 3526005e1c amend 2024-10-30 19:02:22 -07:00
Andrew Pareles 0ef8c66d54 simplify readme 2024-10-30 19:01:56 -07:00
Andrew cd9173c802 move ctrlk 2024-10-30 18:59:17 -07:00
Piyu-Pika 0c8a185df7 Gemini added 2024-10-29 18:05:07 +05:30
Andrew 0f0719c596 fix type 2024-10-29 00:26:46 -07:00
Andrew d9397d870f change CodeSelection type 2024-10-28 17:17:26 -07:00
Andrew 1704a7de7a improve line rounding 2024-10-28 16:34:19 -07:00
Andrew 616cd2e39b update back to enum 2024-10-28 15:58:48 -07:00
Andrew Pareles 222f90bbea Merge pull request #133 from voideditor/multiple-webviews
Max tokens is optional + other config updates - Fixes #128
2024-10-28 15:50:40 -07:00
Andrew Pareles a7a4488848 minor Contributing changes 2024-10-28 04:22:19 -07:00
Andrew Pareles 8e72c1392a Merge pull request #125 from voideditor/multiple-webviews
Speculative Edits
2024-10-28 03:59:53 -07:00
24 changed files with 354 additions and 371 deletions
+6 -7
View File
@@ -69,9 +69,9 @@ We haven't created prerequisite steps for building on Linux yet, but you can fol
### Build instructions
Before building Void, please follow the prerequisite steps above for your operating system. Also make sure you've already built the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
Before building Void, please follow the prerequisite steps above for your operating system. Also make sure you've already built and compiled the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
First, open `void/` in VSCode. Then:
To build Void, first open `void/` in VSCode. Then:
1. Install all dependencies.
@@ -97,8 +97,7 @@ If you ran `npm run watch`, the build is done when you see something like this:
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `/.scripts/code.bat` (Windows). This should open up the built IDE!
To see new changes without restarting the build, press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window.
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
@@ -119,9 +118,7 @@ Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important.
These sometimes get outdated - please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
## ⭐⭐⭐ Make History work well.
@@ -151,6 +148,8 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please don't use AI to write your PR 🙂.
# Relevant files
We keep track of all the files we've changed with Void so it's easy to rebase:
+4 -10
View File
@@ -1,22 +1,16 @@
# Welcome to Void.
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
Void is the open-source Cursor alternative.
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
If you're new, welcome!
## Contributing
To build and run Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## Reference
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository.
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## Support
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
+3 -2
View File
@@ -10,7 +10,8 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"--enable-proposed-api=void.void",
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
@@ -18,4 +19,4 @@
"preLaunchTask": "${defaultBuildTask}"
}
]
}
}
+5
View File
@@ -49,6 +49,11 @@ const convertTSXtoJS = async ({ from, to }) => {
to: 'dist/webviews/ctrlk/index.js',
})
await convertTSXtoJS({
from: 'src/webviews/diffline/index.tsx',
to: 'dist/webviews/diffline/index.js',
})
// convert tailwind to css
await convertTailwindToCSS({
from: 'src/webviews/styles.css',
+19
View File
@@ -10,6 +10,7 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
@@ -737,6 +738,16 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@google/generative-ai": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
"integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -6013,6 +6024,14 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+4
View File
@@ -10,6 +10,9 @@
"categories": [
"Other"
],
"enabledApiProposals": [
"editorInsets"
],
"activationEvents": [],
"main": "./out/extension/extension.js",
"contributes": {
@@ -111,6 +114,7 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
+60 -11
View File
@@ -1,6 +1,7 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../webviews/common/contextForConfig'
export type AbortRef = { current: (() => void) | null }
@@ -37,8 +38,6 @@ type SendLLMMessageFnTypeExternal = (params: {
abortRef: AbortRef,
}) => void
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
@@ -65,7 +64,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
system: systemMessage,
messages: anthropicMessages,
model: voidConfig.anthropic.model,
max_tokens: parseInt(voidConfig.default.maxTokens),
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
let did_abort = false
@@ -103,8 +102,62 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
return { abort }
};
// Gemini
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
abortRef.current = () => {
didAbort = true
}
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// remove system messages that get sent to Gemini
// str of all system messages
let systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
abortRef.current = () => {
// response.stream.return(fullText)
didAbort = true;
}
for await (const chunk of response.stream) {
if (didAbort) return;
const newText = chunk.text();
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError) {
if (error.status === 400) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
}
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
@@ -168,7 +221,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
onError('Invalid API key.');
}
else {
onError(error.message);
onError(`${error.name}:\n${error.message}`);
}
}
else {
@@ -178,7 +231,6 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
@@ -196,7 +248,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
abortRef.current = () => {
@@ -220,8 +272,6 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
@@ -236,7 +286,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
didAbort = true
}
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
@@ -291,8 +340,6 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig) return;
@@ -306,6 +353,8 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText,
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
+10 -5
View File
@@ -2,10 +2,7 @@
import * as vscode from 'vscode';
import { PartialVoidConfig } from '../webviews/common/contextForConfig'
// a selection is a frozen snapshot
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
type File = { filepath: vscode.Uri, content: string }
@@ -35,9 +32,16 @@ type Diff = {
lenses: vscode.CodeLens[],
} & BaseDiff
type Inset = {
text: string,
height: number,
line: number,
insetObj: vscode.WebviewEditorInset,
}
// editor -> sidebar
type MessageToSidebar = (
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
| { type: 'ctrl+k', selection: CodeSelection }
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
@@ -91,6 +95,7 @@ type ChatMessage =
export {
BaseDiff, Diff,
DiffArea,
Inset,
CodeSelection,
File,
MessageFromSidebar,
-210
View File
@@ -1,210 +0,0 @@
import * as vscode from 'vscode';
import { DiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './common/shared_types';
import { v4 as uuidv4 } from 'uuid'
import { AbortRef } from './common/sendLLMMessage';
import { DiffProvider } from './extension/DiffProvider';
import { SidebarWebviewProvider } from './extension/providers/SidebarWebviewProvider';
import { getVoidConfigFromPartial } from './webviews/common/contextForConfig';
import { applyDiffLazily } from './extension/ctrlL';
import { readFileContentOfUri } from './extension/extensionLib/readFileContentOfUri';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
const roundRangeToLines = (selection: vscode.Selection) => {
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
}
export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
);
// 2. ctrl+l
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+l', () => {
const editor = vscode.window.activeTextEditor
if (!editor) return
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
// inset.webview.html = `
// <html>
// <body style="pointer-events:none;">Hello World!</body>
// </html>
// `;
// show the sidebar
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
// get the range of the selection
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
})
);
// 2.5: ctrl+k
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+k', () => {
console.log('CTRLK PRESSED')
const editor = vscode.window.activeTextEditor
if (!editor) return
// get the range of the selection
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject codelens above each change
const diffProvider = new DiffProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
diffProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
diffProvider.rejectDiff(params)
}));
// 5. Receive messages from sidebar
sidebarWebviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
}));
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortApplyRef: AbortRef = { current: null }
if (m.type === 'requestFiles') {
// get contents of all file paths
const files = await Promise.all(
m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
)
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
}
else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showInformationMessage('No active editor!')
return
}
// create an area to show diffs
const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
sweepIndex: null,
}
const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
const docUri = editor.document.uri
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef })
}
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
}
else if (m.type === 'persistPartialVoidConfig') {
const partialVoidConfig = m.partialVoidConfig
context.globalState.update('partialVoidConfig', partialVoidConfig)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else if (m.type === 'getDeviceId') {
let deviceId = context.globalState.get('void_deviceid')
if (!deviceId || typeof deviceId !== 'string') {
deviceId = uuidv4()
context.globalState.update('void_deviceid', deviceId)
}
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
}
else {
console.error('unrecognized command', m)
}
})
}
)
// Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
// TODO need to build this
// const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
// context.subscriptions.push(
// vscode.commands.registerCommand('void.ctrl+k', () => {
// const editor = vscode.window.activeTextEditor;
// if (!editor)
// return
// ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
// // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
// })
// )
}
+107 -39
View File
@@ -1,23 +1,24 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { throttle } from 'lodash';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { DiffArea, BaseDiff, Diff, Inset } from '../common/shared_types';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { updateWebviewHTML } from './extensionLib/updateWebviewHTML';
const THROTTLE_TIME = 100
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
backgroundColor: 'rgba(0, 255, 51, 0.2)',
isWholeLine: true, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)',
backgroundColor: 'rgba(218, 218, 218, 0.2)',
isWholeLine: true,
})
const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgb(148 148 148 / .2)',
backgroundColor: 'rgb(148, 148, 148, 0.2)',
isWholeLine: true,
})
@@ -27,10 +28,14 @@ export class DiffProvider implements vscode.CodeLensProvider {
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _insetsOfDocument: { [docUriStr: string]: Inset[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _extensionUri: vscode.Uri
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
@@ -42,7 +47,8 @@ export class DiffProvider implements vscode.CodeLensProvider {
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
console.log('Creating DisplayChangesProvider')
@@ -182,23 +188,15 @@ export class DiffProvider implements vscode.CodeLensProvider {
// compute the diffs
const diffs = findDiffs(originalCode, currentCode)
// console.log('____________')
// diffs.forEach(diff => {
// console.log('__DELETED: ', diff.originalCode)
// console.log('__INSERTED: ', diff.code)
// })
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.createDiffs(editor.document.uri, diffs, diffArea)
// // print diffs
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
// console.log('!AREA originalCode:', JSON.stringify(originalCode))
// console.log('!AREA currentCode :', JSON.stringify(currentCode))
// for (const diff of this._diffsOfDocument[docUriStr]) {
// console.log('------------')
// console.log('originalCode:', JSON.stringify(diff.originalCode))
// console.log('currentCode:', JSON.stringify(diff.code))
// console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,)
// console.log('currentRange:', diff.range.start.line, diff.range.end.line,)
// }
}
// update green highlighting
@@ -210,18 +208,89 @@ export class DiffProvider implements vscode.CodeLensProvider {
)
);
// update red highlighting
/* NAIVE APPROACH */
if (!this._insetsOfDocument[docUriStr])
this._insetsOfDocument[docUriStr] = []
for (const inset of this._insetsOfDocument[docUriStr]) {
inset.insetObj.dispose()
}
this._insetsOfDocument[docUriStr] = this._diffsOfDocument[docUriStr]
.filter(diff => diff.originalCode !== '')
.map(diff => {
const text = diff.originalCode
const height = diff.originalCode.split('\n').length
const line = diff.range.start.line - 1
const insetObj = vscode.window.createWebviewTextEditorInset(editor, line, height);
updateWebviewHTML(insetObj.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/diffline/index.js', cssOutLocation: 'dist/webviews/styles.css', isCode: true },
{ text }
)
return { text, line, height, insetObj }
})
/* CACHING APPROACH */
// function getCompareString<T extends { [key: string]: any; }>(obj: T, compareKeys: string[]): string {
// return compareKeys.map(key => `${key}:${JSON.stringify(obj?.[key])}`).join('|');
// }
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
darkGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
let s = diffArea.sweepIndex!
return new vscode.Range(s, 0, s, 0)
})
)
)
// function compareArrays<T extends { [key: string]: any }>(arr1: T[], arr2: T[], compareKeys: string[]) {
// const getKey = (obj: T) => getCompareString(obj, compareKeys);
// const set1 = new Set(arr1.map(getKey));
// const set2 = new Set(arr2.map(getKey));
// const common = arr1.filter(obj => set2.has(getKey(obj)))
// const uniqueToFirst = arr1.filter(obj => !set2.has(getKey(obj)))
// const uniqueToSecond = arr2.filter(obj => !set1.has(getKey(obj)))
// const uniqueToFirstKeys = new Set(uniqueToFirst.map(getKey))
// const uniqueToSecondKeys = new Set(uniqueToSecond.map(getKey))
// return {
// common,
// uniqueToFirst,
// uniqueToSecond,
// uniqueToFirstKeys,
// uniqueToSecondKeys,
// getKey,
// };
// }
// compare the current and old insets and update the ones that have changed
// if (!this._insetsOfDocument[docUriStr])
// this._insetsOfDocument[docUriStr] = []
// const oldInsets: Inset[] = this._insetsOfDocument[docUriStr]
// const currInsets: Inset[] = this._diffsOfDocument[docUriStr]
// .map(diff => ({ text: diff.originalCode, height: diff.originalCode.split('\n').length, line: diff.range.start.line - 1, insetObj: undefined! }))
// const {
// common: commonInsets,
// uniqueToFirst: deletedInsets,
// uniqueToSecond: addedInsets,
// } = compareArrays(oldInsets, currInsets, ['text', 'height', 'line'])
// console.log('DELETED INSETS', deletedInsets)
// console.log('ADDED INSETS', addedInsets)
// for (const deletedInset of deletedInsets) { // delete insets in the deleted set
// deletedInset.insetObj.dispose()
// }
// for (const addedInset of addedInsets) { // add insets in the added set
// const insetObj = vscode.window.createWebviewTextEditorInset(editor, addedInset.line, addedInset.height);
// updateWebviewHTML(insetObj.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/diffline/index.js', cssOutLocation: 'dist/webviews/styles.css', isCode: true },
// { text: addedInset.text }
// )
// addedInset.insetObj = insetObj
// }
// this._insetsOfDocument[docUriStr] = commonInsets.concat(addedInsets)
// // for each diffArea, highlight its sweepIndex in dark gray
// editor.setDecorations(
// darkGrayDecoration,
// (this._diffAreasOfDocument[docUriStr]
// .filter(diffArea => diffArea.sweepIndex !== null)
// .map(diffArea => {
// let s = diffArea.sweepIndex!
// return new vscode.Range(s, 0, s, 0)
// })
// )
// )
// for each diffArea, highlight sweepIndex+1...end in light gray
editor.setDecorations(
@@ -234,8 +303,6 @@ export class DiffProvider implements vscode.CodeLensProvider {
)
)
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
@@ -379,7 +446,8 @@ export class DiffProvider implements vscode.CodeLensProvider {
// used by us only
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string, options?: { endStream?: boolean }) => {
const { endStream, } = options || {}
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
@@ -387,12 +455,12 @@ export class DiffProvider implements vscode.CodeLensProvider {
return;
}
// original code all diffs are based on in the code
// original code all diffs are based on
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs[diffs.length - 1] ?? null
const lastDiff = diffs?.[diffs.length - 1]
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
@@ -426,14 +494,14 @@ export class DiffProvider implements vscode.CodeLensProvider {
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea.sweepIndex = newFileEndLine
// replace oldDACode with newDACode with a vscode edit
diffArea.sweepIndex = endStream ? null : newFileEndLine
// replace oldCode with newCode using a vscode edit
const workspaceEdit = new vscode.WorkspaceEdit();
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME)
}
@@ -134,7 +134,7 @@ Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it
// lazily applies the diff to the file
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
export const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
// stateful variables
@@ -179,7 +179,3 @@ const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffR
}
export { applyDiffLazily }
@@ -1,9 +1,9 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "./sendLLMMessage"
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage"
import { VoidConfig } from '../webviews/common/contextForConfig';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../common/systemPrompts';
import { throttle } from 'lodash';
import { readFileContentOfUri } from '../extension/extensionLib/readFileContentOfUri';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
type Res<T> = ((value: T) => void)
+24 -27
View File
@@ -1,13 +1,14 @@
import * as vscode from 'vscode';
import { applyDiffLazily } from './ctrlL';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { DiffProvider } from './DiffProvider';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { v4 as uuidv4 } from 'uuid'
import { AbortRef } from '../common/sendLLMMessage';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { applyDiffLazily } from './applyDiffLazily';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
@@ -25,7 +26,16 @@ declare module 'vscode' {
}
const roundRangeToLines = (selection: vscode.Selection) => {
return new vscode.Range(selection.start.line, 0, selection.end.line, Number.MAX_SAFE_INTEGER)
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
}
const getSelection = (editor: vscode.TextEditor) => {
// get the range of the selection and the file the user is in
const selectionRange = roundRangeToLines(editor.selection);
const selectionStr = editor.document.getText(selectionRange).trim();
const filePath = editor.document.uri;
return { selectionStr, filePath }
}
export function activate(context: vscode.ExtensionContext) {
@@ -50,17 +60,10 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
// get the range of the selection
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
@@ -71,22 +74,16 @@ export function activate(context: vscode.ExtensionContext) {
const editor = vscode.window.activeTextEditor
if (!editor) return
// get the range of the selection
const selectionRange = roundRangeToLines(editor.selection);
// get the text the user is selecting
const selectionStr = editor.document.getText(selectionRange);
// get the file the user is in
const filePath = editor.document.uri;
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
ctrlKWebviewProvider.onPressCtrlK()
// ctrlKWebviewProvider.onPressCtrlK()
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject codelens above each change
const diffProvider = new DiffProvider();
const diffProvider = new DiffProvider(context);
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
@@ -1,14 +1,13 @@
import * as vscode from 'vscode'
export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
const document = await vscode.workspace.openTextDocument(uri);
return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
// export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
// const document = await vscode.workspace.openTextDocument(uri.fsPath);
// return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
// };
};
// this is the old version, which only reads the most recently saved version
// export const readFileContentOfUri = async (uri: vscode.Uri) => {
// return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
// .replace(/\r\n/g, '\n') // replace windows \r\n with \n
// }
// TODO this only accesses the most recently saved version; make it instead access the most recent version in the vscode editor
export const readFileContentOfUri = async (uri: vscode.Uri) => {
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
}
@@ -11,7 +11,7 @@ function generateNonce() {
// call this when you have access to the webview to set its html
export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.Uri, { jsOutLocation, cssOutLocation }: { jsOutLocation: string, cssOutLocation: string }, props?: object) => {
export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.Uri, { jsOutLocation, cssOutLocation, isCode }: { jsOutLocation: string, cssOutLocation: string, isCode?: boolean }, props?: object) => {
// 'dist/sidebar/index.js'
// 'dist/sidebar/styles.css'
@@ -21,6 +21,8 @@ export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri));
const nonce = generateNonce();
const codeStyle = `style="padding-left:0; padding-right:0; font-family: Consolas, &quot;Courier New&quot;, monospace; font-weight: normal; font-size: 14px; font-feature-settings: &quot;liga&quot; 0, &quot;calt&quot; 0; font-variation-settings: normal; line-height: 19px; letter-spacing: 0px;"`
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
@@ -31,7 +33,7 @@ export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<body ${isCode ? codeStyle : ''}>
<div id="root" ${props ? `data-void-props="${encodeURIComponent(JSON.stringify(props))}"` : ''}></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
+1 -1
View File
@@ -127,6 +127,6 @@ export function findDiffs(oldStr: string, newStr: string) {
}
} // end for
console.debug('Replacements', replacements)
// console.debug('Replacements', replacements)
return replacements
}
@@ -3,22 +3,6 @@
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
export class CtrlKWebviewProvider {
@@ -21,11 +21,12 @@ const configString = (description: string, defaultVal: string) => {
export const configFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'ollama',
'openRouter',
'openAICompatible',
'azure'
'azure',
] as const
@@ -46,8 +47,16 @@ const voidConfigInfo: Record<
configFields,
),
maxTokens: configString(
"Max number of tokens to output. Must be a number.", ''
maxTokens: configEnum(
"Max number of tokens to output.",
'1024',
[
"default", // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
@@ -156,6 +165,19 @@ const voidConfigInfo: Record<
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
"gemini-1.5-flash",
"gemini-1.5-pro",
"gemini-1.5-flash-8b",
"gemini-1.0-pro"
] as const
),
},
}
@@ -265,4 +287,3 @@ export function useVoidConfig(): ConfigValueType {
}
return context
}
@@ -1,25 +1,39 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
type PropsType = { [s: string]: any } | null
const PropsContext = createContext<any>(undefined as unknown as any)
type PropsValue = { props: PropsType }
const PropsContext = createContext<PropsValue>(undefined as unknown as PropsValue)
export const getPropsObj = (rootElement: HTMLElement) => {
let props = rootElement.getAttribute("data-void-props")
let propsObj: object | null = null
if (props !== null) {
propsObj = JSON.parse(decodeURIComponent(props))
}
return propsObj
}
// provider for whatever came in data-void-props
export function PropsProvider({ children, props }: { children: ReactNode, props: PropsType }) {
export function PropsProvider({ children, props }: { children: ReactNode, props: object | null }) {
return (
<PropsContext.Provider value={{ props }}>
<PropsContext.Provider value={props}>
{children}
</PropsContext.Provider>
)
}
export function useVoidProps(): PropsValue {
const context = useContext<PropsValue>(PropsContext)
export function useVoidProps<T extends {}>(): T {
// context is the "value" from above
const context: T | null | undefined = useContext<T>(PropsContext)
// only undefined if has no provider
if (context === undefined) {
throw new Error("useVoidProps missing Provider")
}
if (context === null) {
throw new Error("useVoidProps had null props")
}
if (!(context instanceof Object)) {
throw new Error("useVoidProps props was not an object")
}
return context
}
+4 -10
View File
@@ -5,7 +5,7 @@ import { getVSCodeAPI, awaitVSCodeResponse, onMessageFromVSCode } from "./getVsc
import { initPosthog, identifyUser } from "./posthog";
import { ThreadsProvider } from "./contextForThreads";
import { ConfigProvider } from "./contextForConfig";
import { PropsProvider } from "./contextForProps";
import { getPropsObj, PropsProvider } from "./contextForProps";
const ListenersAndTracking = () => {
// initialize posthog
@@ -46,18 +46,12 @@ export const mount = (children: React.ReactNode) => {
// mount the sidebar on the id="root" element
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
let props = rootElement.getAttribute("data-void-props")
let propsObj: object | null = null
if (props !== null) {
propsObj = JSON.parse(decodeURIComponent(props))
}
// console.log("Void root Element:", rootElement)
const content = (<>
<ListenersAndTracking />
<PropsProvider props={propsObj}>
<PropsProvider props={getPropsObj(rootElement)}>
<ThreadsProvider>
<ConfigProvider>
{children}
@@ -69,4 +63,4 @@ export const mount = (children: React.ReactNode) => {
const root = ReactDOM.createRoot(rootElement)
root.render(content);
}
}
@@ -11,6 +11,13 @@ export const CtrlK = () => {
sx('Pressed ctrl+k')
})
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
// inset.webview.html = `
// <html>
// <body style="pointer-events:none;">Hello World!</body>
// </html>
// `;
return <>
<div>
{x}
@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from '../common/getVscodeApi';
import { useVoidProps } from '../common/contextForProps';
type props = {
text: string
}
export const DiffLine = () => {
const props = useVoidProps<props>()
// eslint-disable-next-line react/prop-types
let text = props?.text ?? ''
return <>
<div className="view-line relative pointer-events-none">
<div className={`absolute w-[100%] h-[100%] bg-[rgba(255,0,51,0.2)] z-10`}></div>
<span>
<span className='whitespace-pre text-[14px]'>
{text}
</span>
</span>
</div>
</>
};
@@ -0,0 +1,7 @@
import React from "react"
import { mount } from "../common/mount"
import { DiffLine } from "./DiffLine"
// this is the entry point that mounts diffline
mount(<DiffLine />)
+1 -1
View File
@@ -43,4 +43,4 @@ html {
.dropdown {
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
}
}