Skip to content

Commit bdab4b7

Browse files
fix(web): fix line numbers selectable in Safari (#1037)
* fix(web): fix line numbers selectable in Safari Add WebkitUserSelect: 'none' to prevent line numbers from being selected on Safari, which requires the -webkit-user-select vendor prefix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #1037 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): add copy button to lightweight code highlighter Adds an optional isCopyButtonVisible prop (default false) that renders a copy-to-clipboard button in the top-right corner, visible on hover. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * s --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7e2af39 commit bdab4b7

File tree

3 files changed

+65
-37
lines changed

3 files changed

+65
-37
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Fixed line numbers being selectable in Safari in the lightweight code highlighter. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)
12+
13+
### Added
14+
- Added optional copy button to the lightweight code highlighter (`isCopyButtonVisible` prop), shown on hover. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)
15+
1016
## [4.16.1] - 2026-03-24
1117

1218
### Fixed

packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Parser } from '@lezer/common'
22
import { LanguageDescription, StreamLanguage } from '@codemirror/language'
33
import { Highlighter, highlightTree } from '@lezer/highlight'
44
import { languages as builtinLanguages } from '@codemirror/language-data'
5-
import { memo, useEffect, useMemo, useState } from 'react'
5+
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
66
import { useCodeMirrorHighlighter } from '@/hooks/useCodeMirrorHighlighter'
77
import tailwind from '@/tailwind'
88
import { measure } from '@/lib/utils'
99
import { SourceRange } from '@/features/search'
10+
import { CopyIconButton } from './copyIconButton'
1011

1112
// Define a plain text language
1213
const plainTextLanguage = StreamLanguage.define({
@@ -25,6 +26,7 @@ interface LightweightCodeHighlighter {
2526
/* 1-based line number offset */
2627
lineNumbersOffset?: number;
2728
renderWhitespace?: boolean;
29+
isCopyButtonVisible?: boolean;
2830
}
2931

3032
// The maximum number of characters per line that we will display in the preview.
@@ -46,6 +48,7 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
4648
lineNumbers = false,
4749
lineNumbersOffset = 1,
4850
renderWhitespace = false,
51+
isCopyButtonVisible = false,
4952
} = props;
5053

5154
const unhighlightedLines = useMemo(() => {
@@ -110,6 +113,15 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
110113
isFileTooLargeToDisplay,
111114
]);
112115

116+
const onCopy = useCallback(() => {
117+
try {
118+
navigator.clipboard.writeText(code);
119+
return true;
120+
} catch {
121+
return false;
122+
}
123+
}, [code]);
124+
113125
const lineCount = (highlightedLines ?? unhighlightedLines).length + lineNumbersOffset;
114126
const lineNumberDigits = String(lineCount).length;
115127
const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding
@@ -123,46 +135,55 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
123135
}
124136

125137
return (
126-
<div
127-
style={{
128-
fontFamily: tailwind.theme.fontFamily.editor,
129-
fontSize: tailwind.theme.fontSize.editor,
130-
whiteSpace: renderWhitespace ? 'pre-wrap' : 'none',
131-
wordBreak: 'break-all',
132-
}}
133-
>
134-
{(highlightedLines ?? unhighlightedLines).map((line, index) => (
135-
<div
136-
key={index}
137-
className="flex"
138-
>
139-
{lineNumbers && (
138+
<div className="relative group">
139+
{isCopyButtonVisible && (
140+
<CopyIconButton
141+
onCopy={onCopy}
142+
className="absolute top-1 right-1 z-10 opacity-0 group-hover:opacity-100 transition-opacity group-hover:bg-background"
143+
/>
144+
)}
145+
<div
146+
style={{
147+
fontFamily: tailwind.theme.fontFamily.editor,
148+
fontSize: tailwind.theme.fontSize.editor,
149+
whiteSpace: renderWhitespace ? 'pre-wrap' : 'none',
150+
wordBreak: 'break-all',
151+
}}
152+
>
153+
{(highlightedLines ?? unhighlightedLines).map((line, index) => (
154+
<div
155+
key={index}
156+
className="flex"
157+
>
158+
{lineNumbers && (
159+
<span
160+
style={{
161+
width: lineNumberWidth,
162+
minWidth: lineNumberWidth,
163+
display: 'inline-block',
164+
textAlign: 'left',
165+
paddingLeft: '5px',
166+
userSelect: 'none',
167+
WebkitUserSelect: 'none',
168+
fontFamily: tailwind.theme.fontFamily.editor,
169+
color: tailwind.theme.colors.editor.gutterForeground,
170+
}}
171+
>
172+
{index + lineNumbersOffset}
173+
</span>
174+
)}
140175
<span
141176
style={{
142-
width: lineNumberWidth,
143-
minWidth: lineNumberWidth,
144-
display: 'inline-block',
145-
textAlign: 'left',
146-
paddingLeft: '5px',
147-
userSelect: 'none',
148-
fontFamily: tailwind.theme.fontFamily.editor,
149-
color: tailwind.theme.colors.editor.gutterForeground,
177+
flex: 1,
178+
paddingLeft: '6px',
179+
paddingRight: '2px',
150180
}}
151181
>
152-
{index + lineNumbersOffset}
182+
{line}
153183
</span>
154-
)}
155-
<span
156-
style={{
157-
flex: 1,
158-
paddingLeft: '6px',
159-
paddingRight: '2px',
160-
}}
161-
>
162-
{line}
163-
</span>
164-
</div>
165-
))}
184+
</div>
185+
))}
186+
</div>
166187
</div>
167188
)
168189
})
@@ -184,7 +205,7 @@ async function getCodeParser(
184205
return null;
185206
}
186207

187-
if (!found.support) {
208+
if (!found.support) {
188209
await found.load();
189210
}
190211
return found.support ? found.support.language.parser : null;

packages/web/src/features/chat/components/chatThread/codeBlock.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const CodeBlock = ({
4141
language={language}
4242
lineNumbers={true}
4343
renderWhitespace={true}
44+
isCopyButtonVisible={true}
4445
>
4546
{code}
4647
</LightweightCodeHighlighter>

0 commit comments

Comments
 (0)