diff --git a/CHANGELOG.md b/CHANGELOG.md index 422e90749..12f4b92ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed line numbers being selectable in Safari in the lightweight code highlighter. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037) + +### Added +- Added optional copy button to the lightweight code highlighter (`isCopyButtonVisible` prop), shown on hover. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037) + ## [4.16.1] - 2026-03-24 ### Fixed diff --git a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx index f72e64854..f1d922cf9 100644 --- a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx +++ b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx @@ -2,11 +2,12 @@ import { Parser } from '@lezer/common' import { LanguageDescription, StreamLanguage } from '@codemirror/language' import { Highlighter, highlightTree } from '@lezer/highlight' import { languages as builtinLanguages } from '@codemirror/language-data' -import { memo, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useCodeMirrorHighlighter } from '@/hooks/useCodeMirrorHighlighter' import tailwind from '@/tailwind' import { measure } from '@/lib/utils' import { SourceRange } from '@/features/search' +import { CopyIconButton } from './copyIconButton' // Define a plain text language const plainTextLanguage = StreamLanguage.define({ @@ -25,6 +26,7 @@ interface LightweightCodeHighlighter { /* 1-based line number offset */ lineNumbersOffset?: number; renderWhitespace?: boolean; + isCopyButtonVisible?: boolean; } // The maximum number of characters per line that we will display in the preview. @@ -46,6 +48,7 @@ export const LightweightCodeHighlighter = memo((prop lineNumbers = false, lineNumbersOffset = 1, renderWhitespace = false, + isCopyButtonVisible = false, } = props; const unhighlightedLines = useMemo(() => { @@ -110,6 +113,15 @@ export const LightweightCodeHighlighter = memo((prop isFileTooLargeToDisplay, ]); + const onCopy = useCallback(() => { + try { + navigator.clipboard.writeText(code); + return true; + } catch { + return false; + } + }, [code]); + const lineCount = (highlightedLines ?? unhighlightedLines).length + lineNumbersOffset; const lineNumberDigits = String(lineCount).length; const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding @@ -123,46 +135,55 @@ export const LightweightCodeHighlighter = memo((prop } return ( -
- {(highlightedLines ?? unhighlightedLines).map((line, index) => ( -
- {lineNumbers && ( +
+ {isCopyButtonVisible && ( + + )} +
+ {(highlightedLines ?? unhighlightedLines).map((line, index) => ( +
+ {lineNumbers && ( + + {index + lineNumbersOffset} + + )} - {index + lineNumbersOffset} + {line} - )} - - {line} - -
- ))} +
+ ))} +
) }) @@ -184,7 +205,7 @@ async function getCodeParser( return null; } - if (!found.support) { + if (!found.support) { await found.load(); } return found.support ? found.support.language.parser : null; diff --git a/packages/web/src/features/chat/components/chatThread/codeBlock.tsx b/packages/web/src/features/chat/components/chatThread/codeBlock.tsx index ce9aacd63..45b02c07f 100644 --- a/packages/web/src/features/chat/components/chatThread/codeBlock.tsx +++ b/packages/web/src/features/chat/components/chatThread/codeBlock.tsx @@ -41,6 +41,7 @@ export const CodeBlock = ({ language={language} lineNumbers={true} renderWhitespace={true} + isCopyButtonVisible={true} > {code}