monaco loads... need to adjust height

This commit is contained in:
Andrew 2024-10-19 21:38:17 -07:00
parent a49a1324a5
commit 09d84044b5
10 changed files with 199 additions and 225 deletions

View file

@ -5,6 +5,13 @@ esbuild.build({
entryPoints: ['src/sidebar/index.tsx'],
bundle: true,
minify: true,
loader: {
'.ttf': 'file',
'.woff': 'file',
'.woff2': 'file',
'.eot': 'file',
'.svg': 'file',
},
sourcemap: true,
outfile: 'dist/sidebar/index.js',
format: 'iife', // apparently iife is safe for browsers (safer than cjs)

View file

@ -101,7 +101,7 @@
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"vscode:prepublish": "echo \"running prepublish\"",
"watch": "tsc -watch -p ./",
"build": "rimraf dist && node build-tsx.js && node build-css.js",
"pretest": "tsc -p ./ && eslint src --ext ts",

View file

@ -65,7 +65,8 @@ type MessageFromSidebar = (
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
}
}

View file

@ -83,7 +83,8 @@ export function activate(context: vscode.ExtensionContext) {
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
} else if (m.type === 'applyChanges') {
}
else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {

View file

@ -4,7 +4,6 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from "reac
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import { SelectedFiles } from "./components/SelectedFiles";
import { File, ChatMessage, CodeSelection } from "../common/shared_types";
import * as vscode from 'vscode'
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
@ -63,6 +62,55 @@ Please edit the selected code following these instructions:
return str;
};
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
@ -76,16 +124,17 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{chatMessage.selection?.selectionStr && <BlockCode
text={chatMessage.selection.selectionStr}
buttonsOnHover={null}
/>}
{children}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
{chatbubbleContents}
@ -224,8 +273,8 @@ export const SidebarChat = () => {
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
const llmContent = messageStream || '(null)'
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
@ -233,14 +282,9 @@ export const SidebarChat = () => {
}, [captureChatEvent, messageStream, addMessageToHistory])
//Clear code selection
const clearSelection = () => {
setSelection(null);
};
return <>
<div className="overflow-y-auto overflow-x-hidden space-y-4">
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
@ -261,14 +305,15 @@ export const SidebarChat = () => {
<SelectedFiles files={files} setFiles={setFiles} />
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button
onClick={clearSelection}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
<BlockCode text={selection.selectionStr}
buttonsOnHover={(
<button
onClick={() => setSelection(null)}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}

View file

@ -15,7 +15,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { allThreads, currentThread, switchToThread } = useThreads()
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].createdAt > allThreads![threadId2].createdAt ? -1 : 1)
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
return (
<div className="flex flex-col gap-y-1">

View file

@ -1,48 +0,0 @@
import React from "react"
import * as vscode from "vscode"
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}

View file

@ -14,11 +14,15 @@ type ConfigForThreadsValueType = {
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
const createNewThread = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
messages: [],
}
}
// const [stateRef, setState] = useInstantState(initVal)
@ -67,6 +71,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
...allThreads.current,
[currentThread.id]: {
...currentThread,
lastModified: new Date().toISOString(),
messages: [...currentThread.messages, message],
}
})

View file

@ -1,162 +1,70 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
import MonacoEditor from '@monaco-editor/react'
import { editor } from 'monaco-editor'
import * as monaco from 'monaco-editor';
import { loader } from '@monaco-editor/react';
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
loader.config({ monaco });
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
language,
toolbar,
hideToolbar = false,
className,
}: {
text: string
language?: string
toolbar?: ReactNode
hideToolbar?: boolean
className?: string
}) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const customStyle = {
...atomOneDarkReasonable,
'code[class*="language-"]': {
...atomOneDarkReasonable['code[class*="language-"]'],
background: "none",
},
}
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
const defaultToolbar = (
<>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
}}
>
Apply
</button>
</>
)
const BlockCode = ({ text, language, buttonsOnHover, }: { text: string, language?: string, buttonsOnHover?: ReactNode, }) => {
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden`}>
<MonacoEditor
onMount={(editor, monaco) => {
const model = editor.getModel()
model?.setEOL(monaco.editor.EndOfLineSequence.LF)
// model?.updateOptions({ tabSize: 4 }) // apparently this should get set on the model, not the editor ()
monaco?.editor.setTheme('dark')
}}
loading=''
value={text}
defaultValue={text}
language={language}
defaultLanguage={language}
// onChange={() => { onChangeText?.() }}
height={'100%'} // 100% or the exact pixel height
theme={'dark'}
options={{
matchBrackets: 'always',
detectIndentation: false, // we always want a tab size of 4
tabSize: 4,
insertSpaces: true,
// fontSize: 15,
wordWrapColumn: 10000, // we want this to be infinity
// automaticLayout: true,
wordWrap: 'bounded', // 'off'
// wordBreak: 'keepAll',
// automaticLayout: true,
// lineDecorationsWidth: 0,
lineNumbersMinChars: 4,
lineNumbers: 'off',
renderLineHighlight: 'none',
minimap: { enabled: false },
scrollBeyondLastColumn: 0,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: false, //height !== undefined
// vertical: 'hidden',
// horizontal: 'hidden'
},
overviewRulerLanes: 0,
readOnly: true,
readOnlyMessage: undefined,
quickSuggestions: false,
}}
/>
<div className="relative group">
{!hideToolbar && (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-500">
<div className="flex space-x-2 p-2">{toolbar || defaultToolbar}</div>
{!toolbar ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
>
<SyntaxHighlighter
language={language}
style={customStyle}
className={"rounded-sm"}
>
{text}
</SyntaxHighlighter>
</div>
<MonacoEditor
onMount={(editor, monaco) => {
const model = editor.getModel()
model?.setEOL(monaco.editor.EndOfLineSequence.LF)
monaco?.editor.setTheme('vs-dark')
}}
loading='loading'
value={text}
language={language}
// onChange={() => { onChangeText?.() }}
height={'100%'} // 100% or the exact pixel height
theme={'vs-dark'}
options={{
matchBrackets: 'always',
detectIndentation: false, // we always want a tab size of 4
tabSize: 4,
insertSpaces: true,
// fontSize: 15,
wordWrapColumn: 10000, // we want this to be infinity
// automaticLayout: true,
wordWrap: 'bounded', // 'off'
// wordBreak: 'keepAll',
// automaticLayout: true,
// lineDecorationsWidth: 0,
lineNumbersMinChars: 4,
lineNumbers: 'off',
renderLineHighlight: 'none',
minimap: { enabled: false },
scrollBeyondLastColumn: 0,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: true, // height !== undefined
// vertical: 'hidden',
// horizontal: 'hidden'
},
overviewRulerLanes: 0,
readOnly: true,
readOnlyMessage: undefined,
quickSuggestions: false,
}}
/>
</div>
</>
)

View file

@ -1,6 +1,57 @@
import React, { JSX } from "react"
import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
return <>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
}}
>
Apply
</button>
</>
}
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
@ -12,7 +63,11 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
}
if (t.type === "code") {
return <BlockCode text={t.text} language={t.lang} />
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
if (t.type === "heading") {