Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed line numbers being selectable in Safari in the lightweight code highlighter. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)
- Fixed GitLab sync deleting repos when the API returns a non-404 error (e.g. 500) during group/user/project fetch. [#1039](https://github.com/sourcebot-dev/sourcebot/pull/1039)
- Fixed React hydration mismatch in `KeyboardShortcutHint` caused by platform detection running at module load time during SSR. [#1041](https://github.com/sourcebot-dev/sourcebot/pull/1041)
- Fixed rendering performance for ask threads, especially when hovering or selecting citations. [#1042](https://github.com/sourcebot-dev/sourcebot/pull/1042)

### Added
- Added optional copy button to the lightweight code highlighter (`isCopyButtonVisible` prop), shown on hover. [#1037](https://github.com/sourcebot-dev/sourcebot/pull/1037)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { DuplicateChatDialog } from '@/app/[domain]/chat/components/duplicateCha
import { LoginModal } from '@/app/components/loginModal';
import type { IdentityProviderMetadata } from '@/lib/identityProviders';
import { getAskGhLoginWallData } from '../../actions';
import { useParams } from 'next/navigation';
import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants';
Comment thread
brendan-kellam marked this conversation as resolved.

type ChatHistoryState = {
scrollOffset?: number;
Expand Down Expand Up @@ -71,7 +71,6 @@ export const ChatThread = ({
const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: false });
const { toast } = useToast();
const router = useRouter();
const params = useParams<{ domain: string }>();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
Expand Down Expand Up @@ -341,9 +340,9 @@ export const ChatThread = ({
}

captureEvent('wa_chat_duplicated', { chatId: defaultChatId });
router.push(`/${params.domain}/chat/${result.id}`);
router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${result.id}`);
return result.id;
}, [defaultChatId, toast, router, params.domain, captureEvent]);
}, [defaultChatId, toast, router, captureEvent]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,25 @@ import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/sy
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency";
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { cn } from "@/lib/utils";
import { Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
import { EditorView } from '@codemirror/view';
import { CodeHostType } from "@sourcebot/db";
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import isEqual from "fast-deep-equal/react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { forwardRef, memo, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react";
import { forwardRef, memo, Ref, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { FileReference } from "../../types";
import { createCodeFoldingExtension } from "./codeFoldingExtension";
import { createReferencesHighlightExtension, setHoveredIdEffect, setSelectedIdEffect } from "./referencesHighlightExtension";

const lineDecoration = Decoration.line({
attributes: { class: "cm-range-border-radius chat-lineHighlight" },
});

const selectedLineDecoration = Decoration.line({
attributes: { class: "cm-range-border-radius cm-range-border-shadow chat-lineHighlight-selected" },
});

const hoverLineDecoration = Decoration.line({
attributes: { class: "chat-lineHighlight-hover" },
});

const CODEMIRROR_BASIC_SETUP = {
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
foldKeymap: false,
} as const;

interface ReferencedFileSourceListItemProps {
id: string;
Expand Down Expand Up @@ -75,47 +70,32 @@ const ReferencedFileSourceListItemComponent = ({
forwardedRef,
() => editorRef as ReactCodeMirrorRef
);

const keymapExtension = useKeymapExtension(editorRef?.view);
const hasCodeNavEntitlement = useHasEntitlement("code-nav");

const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view);

const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => {
const pos = view.posAtCoords({ x, y });
if (pos === null) return undefined;

// Check if position is within the main editor content area
const rect = view.contentDOM.getBoundingClientRect();
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
return undefined;
}

const line = view.state.doc.lineAt(pos);
const lineNumber = line.number;

// Check if this line is part of any highlighted range
const matchingRanges = references.filter(({ range }) =>
range && lineNumber >= range.startLine && lineNumber <= range.endLine
);
const codeFoldingExtension = useMemo(() => {
return createCodeFoldingExtension(references, 3);
}, [references]);

// Sort by the length of the range.
// Shorter ranges are more specific, so we want to prioritize them.
matchingRanges.sort((a, b) => {
const aLength = (a.range!.endLine) - (a.range!.startLine);
const bLength = (b.range!.endLine) - (b.range!.startLine);
return aLength - bLength;
});
const referencesHighlightExtension = useExtensionWithDependency(
editorRef?.view ?? null,
() => createReferencesHighlightExtension(references, onHoveredReferenceChanged, onSelectedReferenceChanged),
[references],
);

if (matchingRanges.length > 0) {
return matchingRanges[0];
useEffect(() => {
if (editorRef?.view) {
editorRef.view.dispatch({ effects: setHoveredIdEffect.of(hoveredReference?.id) });
}
}, [hoveredReference?.id, editorRef?.view]);

return undefined;
}, [references]);

const codeFoldingExtension = useMemo(() => {
return createCodeFoldingExtension(references, 3);
}, [references]);
useEffect(() => {
if (editorRef?.view) {
editorRef.view.dispatch({ effects: setSelectedIdEffect.of(selectedReference?.id) });
}
}, [selectedReference?.id, editorRef?.view]);

const extensions = useMemo(() => {
return [
Expand All @@ -126,88 +106,14 @@ const ReferencedFileSourceListItemComponent = ({
symbolHoverTargetsExtension,
] : []),
codeFoldingExtension,
StateField.define<DecorationSet>({
create(state) {
const decorations: Range<Decoration>[] = [];

for (const { range, id } of references) {
if (!range) {
continue;
}

const isHovered = id === hoveredReference?.id;
const isSelected = id === selectedReference?.id;

for (let line = range.startLine; line <= range.endLine; line++) {
// Skip lines that are outside the document bounds.
if (line > state.doc.lines) {
continue;
}

if (isSelected) {
decorations.push(selectedLineDecoration.range(state.doc.line(line).from));
} else {
decorations.push(lineDecoration.range(state.doc.line(line).from));
if (isHovered) {
decorations.push(hoverLineDecoration.range(state.doc.line(line).from));
}
}

}
}

return Decoration.set(decorations, /* sort = */ true);
},
update(deco, tr) {
return deco.map(tr.changes);
},
provide: (field) => EditorView.decorations.from(field),
}),
EditorView.domEventHandlers({
click: (event, view) => {
const reference = getReferenceAtPos(event.clientX, event.clientY, view);

if (reference) {
onSelectedReferenceChanged(reference.id === selectedReference?.id ? undefined : reference);
return true; // prevent default handling
}
return false;
},
mouseover: (event, view) => {
const reference = getReferenceAtPos(event.clientX, event.clientY, view);
if (!reference) {
return false;
}

if (reference.id === selectedReference?.id || reference.id === hoveredReference?.id) {
return false;
}

onHoveredReferenceChanged(reference);
return true;
},
mouseout: (event, view) => {
const reference = getReferenceAtPos(event.clientX, event.clientY, view);
if (reference) {
return false;
}

onHoveredReferenceChanged(undefined);
return true;
}
})
referencesHighlightExtension,
];
}, [
languageExtension,
keymapExtension,
hasCodeNavEntitlement,
references,
hoveredReference?.id,
selectedReference?.id,
getReferenceAtPos,
onSelectedReferenceChanged,
onHoveredReferenceChanged,
codeFoldingExtension,
referencesHighlightExtension,
]);

const ExpandCollapseIcon = useMemo(() => {
Expand Down Expand Up @@ -253,12 +159,7 @@ const ReferencedFileSourceListItemComponent = ({
extensions={extensions}
readOnly={true}
theme={theme}
basicSetup={{
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
foldKeymap: false,
}}
basicSetup={CODEMIRROR_BASIC_SETUP}
>
{editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup
Expand Down
Loading
Loading