Copy code block (#47)

* rearrange markdown components

* add copy button

* add default style for small button

* hide entire toolbar in user messages

* finish merge

* refactor

---------

Co-authored-by: Andrew <andrewpareles@gmail.com>
This commit is contained in:
anetaj 2024-10-12 01:56:44 +02:00 committed by GitHub
parent e2f337f882
commit 42bdcf54ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 262 additions and 181 deletions

View file

@ -1,160 +0,0 @@
import React, { JSX, useState } from 'react';
import { MarkedToken, Token, TokensList } from 'marked';
import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi';
// code block with Apply button at top
export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => {
return <div className='py-1'>
{disableApplyButton ? null : <div className='text-sm'>
<button className='btn btn-secondary px-3 py-1 text-sm rounded-t-sm'
onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyCode', code: text }) }}>Apply</button>
</div>}
<div className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${disableApplyButton ? '' : 'rounded-tl-none'}`}>
<pre className='p-3'>
{text}
</pre>
</div>
</div>
}
const Render = ({ token }: { token: Token }) => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>;
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
return <HeadingTag>{t.text}</HeadingTag>;
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || 'left' }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td key={cellIndex} style={{ textAlign: t.align[cellIndex] || 'left' }}>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
if (t.type === "hr") {
return <hr />;
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>;
}
if (t.type === "list") {
const ListTag = t.ordered ? 'ol' : 'ul';
return (
<ListTag start={t.start !== '' ? t.start : undefined}
className={`list-inside ${t.ordered ? 'list-decimal' : 'list-disc'}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
{item.text}
</li>
))}
</ListTag>
);
}
if (t.type === "paragraph") {
return <p>
{t.tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</p>;
}
if (t.type === "html") {
return <pre>{`<html>`}{t.raw}{`</html>`}</pre>;
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>;
}
if (t.type === "def") {
return null; // Definitions are typically not rendered
}
if (t.type === "link") {
return <a href={t.href} title={t.title ?? undefined}>{t.text}</a>;
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />;
}
if (t.type === "strong") {
return <strong>{t.text}</strong>;
}
if (t.type === "em") {
return <em>{t.text}</em>;
}
// inline code
if (t.type === "codespan") {
return <code className='text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono'>{t.text}</code>;
}
if (t.type === "br") {
return <br />;
}
if (t.type === "del") {
return <del>{t.text}</del>;
}
// default
return <div className='bg-orange-50 rounded-sm overflow-hidden'>
<span className='text-xs text-orange-500'>Unknown type:</span>
{t.raw}
</div>;
};
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
return (
<>
{tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</>
);
};
export default MarkdownRender;

View file

@ -4,7 +4,8 @@ import { Command, File, Selection, WebviewMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked';
import MarkdownRender, { BlockCode } from "./MarkdownRender";
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import * as vscode from 'vscode'
@ -82,7 +83,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') {
chatbubbleContents = <>
<IncludedFiles files={chatMessage.files} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children}
</>
}
@ -278,7 +279,7 @@ const Sidebar = () => {
>
X
</button>
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
<BlockCode text={selection.selectionStr} hideToolbar />
</div>
)}
</div>

View file

@ -0,0 +1,72 @@
import React, { useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
hideToolbar = false,
}: {
text: string
hideToolbar?: boolean
}) => {
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 (
<div className="relative group">
{!hideToolbar && (
<div className="absolute top-0 right-0 invisible group-hover:visible">
<div className="flex space-x-2 p-2">
<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: "applyCode", code: text })
}}
>
Apply
</button>
</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${hideToolbar ? "" : "rounded-tl-none"}`}
>
<pre className="p-4">{text}</pre>
</div>
</div>
)
}
export default BlockCode

View file

@ -0,0 +1,164 @@
import React, { JSX, useState } from "react"
import { MarkedToken, Token, TokensList } from "marked"
import { awaitVSCodeResponse, getVSCodeAPI } from "../getVscodeApi"
import BlockCode from "./BlockCode"
const Render = ({ token }: { token: Token }) => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>
}
if (t.type === "code") {
return <BlockCode text={t.text} />
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
return <HeadingTag>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
if (t.type === "hr") {
return <hr />
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
return (
<ListTag
start={t.start !== "" ? t.start : undefined}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
{item.text}
</li>
))}
</ListTag>
)
}
if (t.type === "paragraph") {
return (
<p>
{t.tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</p>
)
}
if (t.type === "html") {
return (
<pre>
{`<html>`}
{t.raw}
{`</html>`}
</pre>
)
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>
}
if (t.type === "def") {
return null // Definitions are typically not rendered
}
if (t.type === "link") {
return (
<a href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
}
if (t.type === "strong") {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
}
if (t.type === "br") {
return <br />
}
if (t.type === "del") {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
return (
<>
{tokens.map((token, index) => (
<Render key={index} token={token} />
))}
</>
)
}
export default MarkdownRender

View file

@ -3,33 +3,37 @@
@tailwind utilities;
html {
font-size: var(--vscode-font-size);
font-size: var(--vscode-font-size);
}
.btn {
@apply cursor-pointer transition-colors;
@apply cursor-pointer transition-colors;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&.btn-sm {
@apply px-3 py-1 text-sm;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
}
.input {
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
}