diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer/LightDiffViewer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer/LightDiffViewer.tsx index aee39018232..3b64ab44c2a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer/LightDiffViewer.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/components/LightDiffViewer/LightDiffViewer.tsx @@ -61,6 +61,12 @@ export function LightDiffViewer({ themeType: "dark", overflow: "wrap", disableFileHeader: true, + unsafeCSS: ` + * { + user-select: text; + -webkit-user-select: text; + } + `, }} /> ); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx index d46a6061abf..4901ed2c98b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/FileViewerPane.tsx @@ -173,7 +173,13 @@ export function FileViewerPane({ pinPane(paneId); }; - const switchToMode = (newMode: FileViewerMode) => { + const switchToMode = ( + newMode: FileViewerMode, + location?: { + line?: number; + column?: number; + }, + ) => { const panes = useTabsStore.getState().panes; const currentPane = panes[paneId]; if (currentPane?.fileViewer) { @@ -185,6 +191,9 @@ export function FileViewerPane({ fileViewer: { ...currentPane.fileViewer, viewMode: newMode, + initialLine: location?.line ?? currentPane.fileViewer.initialLine, + initialColumn: + location?.column ?? currentPane.fileViewer.initialColumn, }, }, }, @@ -192,6 +201,10 @@ export function FileViewerPane({ } }; + const handleSwitchToRawAtLocation = (line: number, column: number) => { + switchToMode("raw", { line, column }); + }; + const handleViewModeChange = (value: string) => { if (!value) return; const newMode = value as FileViewerMode; @@ -300,6 +313,7 @@ export function FileViewerPane({ onSaveRaw={handleSaveRaw} onEditorChange={handleEditorChange} setIsDirty={setIsDirty} + onSwitchToRawAtLocation={handleSwitchToRawAtLocation} // Context menu props onSplitHorizontal={() => splitPaneHorizontal(tabId, paneId, path)} onSplitVertical={() => splitPaneVertical(tabId, paneId, path)} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx new file mode 100644 index 00000000000..51579ebcf5d --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx @@ -0,0 +1,149 @@ +import { ContextMenuItem } from "@superset/ui/context-menu"; +import { toast } from "@superset/ui/sonner"; +import { type MutableRefObject, type ReactNode, useCallback } from "react"; +import { LuSquarePen } from "react-icons/lu"; +import type { Tab } from "renderer/stores/tabs/types"; +import { + type CodeEditorAdapter, + EditorContextMenu, + type EditorSelectionLines, + useEditorActions, +} from "../../../../../components"; + +interface DiffViewerContextMenuProps { + children: ReactNode; + containerRef: MutableRefObject; + filePath: string; + getSelectionLines: () => EditorSelectionLines | null; + onSplitHorizontal: () => void; + onSplitVertical: () => void; + onSplitWithNewChat?: () => void; + onSplitWithNewBrowser?: () => void; + onClosePane: () => void; + currentTabId: string; + availableTabs: Tab[]; + onMoveToTab: (tabId: string) => void; + onMoveToNewTab: () => void; + onEditAtLocation: () => void; +} + +function isSelectionInsideContainer( + container: HTMLDivElement, + selection: Selection, +): boolean { + if (selection.rangeCount === 0) { + return false; + } + + for (let index = 0; index < selection.rangeCount; index += 1) { + const range = selection.getRangeAt(index); + if (!container.contains(range.commonAncestorContainer)) { + return false; + } + } + + return true; +} + +export function DiffViewerContextMenu({ + children, + containerRef, + filePath, + getSelectionLines, + onSplitHorizontal, + onSplitVertical, + onSplitWithNewChat, + onSplitWithNewBrowser, + onClosePane, + currentTabId, + availableTabs, + onMoveToTab, + onMoveToNewTab, + onEditAtLocation, +}: DiffViewerContextMenuProps) { + const getEditor = useCallback((): CodeEditorAdapter | null => { + const container = containerRef.current; + if (!container) { + return null; + } + + return { + focus() { + container.focus(); + }, + getValue() { + return container.innerText; + }, + setValue(_value: string) {}, + revealPosition(_line: number, _column?: number) {}, + getSelectionLines, + selectAll() { + const selection = window.getSelection(); + if (!selection) return; + const range = document.createRange(); + range.selectNodeContents(container); + selection.removeAllRanges(); + selection.addRange(range); + }, + cut() {}, + copy() { + const selection = window.getSelection(); + if (!selection || !isSelectionInsideContainer(container, selection)) { + return; + } + + const selectedText = selection.toString(); + if (!selectedText) { + return; + } + + void navigator.clipboard.writeText(selectedText).catch((error) => { + console.error( + "[DiffViewerContextMenu] Failed to copy selection:", + error, + ); + toast.error("Failed to copy selection", { + description: String(error), + }); + }); + }, + paste() {}, + openFind() {}, + dispose() {}, + }; + }, [containerRef, getSelectionLines]); + + const editorActions = useEditorActions({ + getEditor, + filePath, + editable: false, + }); + + return ( + + + Edit Here + + } + paneActions={{ + onSplitHorizontal, + onSplitVertical, + onSplitWithNewChat, + onSplitWithNewBrowser, + onClosePane, + currentTabId, + availableTabs, + onMoveToTab, + onMoveToNewTab, + }} + > + {children} + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/index.ts new file mode 100644 index 00000000000..10518fdbde1 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/index.ts @@ -0,0 +1 @@ +export { DiffViewerContextMenu } from "./DiffViewerContextMenu"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx index 51ab641849d..e2c5c15e66d 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/FileViewerContent.tsx @@ -14,8 +14,15 @@ import type { DiffViewMode } from "shared/changes-types"; import { detectLanguage } from "shared/detect-language"; import { isImageFile } from "shared/file-types"; import type { FileViewerMode } from "shared/tabs-types"; +import { DiffViewerContextMenu } from "../DiffViewerContextMenu"; import { FileEditorContextMenu } from "../FileEditorContextMenu"; import { MarkdownSearch } from "../MarkdownSearch"; +import { + type DiffDomLocation, + getColumnFromDiffPoint, + getDiffLocationFromEvent, + mapDiffLocationToRawPosition, +} from "./utils/diff-location"; interface RawFileData { ok: true; @@ -58,6 +65,33 @@ interface DiffData { language: string; } +function hasActiveSelectionWithinElement( + element: HTMLDivElement | null, +): boolean { + if (!element) { + return false; + } + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + return false; + } + + const text = selection.toString(); + if (text.length === 0) { + return false; + } + + for (let index = 0; index < selection.rangeCount; index += 1) { + const range = selection.getRangeAt(index); + if (element.contains(range.commonAncestorContainer)) { + return true; + } + } + + return false; +} + interface FileViewerContentProps { viewMode: FileViewerMode; filePath: string; @@ -77,6 +111,7 @@ interface FileViewerContentProps { onSaveRaw: () => Promise; onEditorChange: (value: string | undefined) => void; setIsDirty: (dirty: boolean) => void; + onSwitchToRawAtLocation: (line: number, column: number) => void; onSplitHorizontal: () => void; onSplitVertical: () => void; onSplitWithNewChat?: () => void; @@ -120,6 +155,7 @@ export function FileViewerContent({ onSaveRaw, onEditorChange, setIsDirty, + onSwitchToRawAtLocation, onSplitHorizontal, onSplitVertical, onSplitWithNewChat, @@ -134,6 +170,13 @@ export function FileViewerContent({ }: FileViewerContentProps) { const isImage = isImageFile(filePath); const hasAppliedInitialLocationRef = useRef(false); + const diffContainerRef = useRef(null); + const lastDiffLocationRef = useRef< + | (DiffDomLocation & { + column?: number; + }) + | null + >(null); // biome-ignore lint/correctness/useExhaustiveDependencies: Reset on file change only useEffect(() => { @@ -145,6 +188,63 @@ export function FileViewerContent({ hasAppliedInitialLocationRef.current = false; }, [initialLine, initialColumn]); + useEffect(() => { + if (viewMode !== "raw") { + hasAppliedInitialLocationRef.current = false; + } + }, [viewMode]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: Reset cached diff interaction when the rendered diff changes + useEffect(() => { + lastDiffLocationRef.current = null; + }, [ + filePath, + diffData?.original, + diffData?.modified, + diffViewMode, + hideUnchangedRegions, + ]); + + const getDiffSelectionLines = () => { + if (!diffData || !lastDiffLocationRef.current) { + return null; + } + + const position = mapDiffLocationToRawPosition({ + contents: diffData, + lineNumber: lastDiffLocationRef.current.lineNumber, + side: lastDiffLocationRef.current.side, + lineType: lastDiffLocationRef.current.lineType, + }); + + return { + startLine: position.lineNumber, + endLine: position.lineNumber, + }; + }; + + const openRawFromDiffLocation = ( + location: DiffDomLocation & { + column: number; + }, + ) => { + if (!diffData) { + return; + } + + lastDiffLocationRef.current = location; + + const position = mapDiffLocationToRawPosition({ + contents: diffData, + lineNumber: location.lineNumber, + side: location.side, + lineType: location.lineType, + column: location.column, + }); + + onSwitchToRawAtLocation(position.lineNumber, position.column); + }; + useEffect(() => { if (viewMode !== "raw") return; if (isLoadingRaw) return; @@ -203,16 +303,68 @@ export function FileViewerContent({ } return ( -
- -
+ { + const location = lastDiffLocationRef.current; + if (!location || location.column === undefined) { + return; + } + + openRawFromDiffLocation({ + ...location, + column: location.column, + }); + }} + > +
{ + if (hasActiveSelectionWithinElement(diffContainerRef.current)) { + event.stopPropagation(); + } + }} + onContextMenuCapture={(event) => { + const location = getDiffLocationFromEvent(event.nativeEvent); + if (!location) { + return; + } + + const column = getColumnFromDiffPoint({ + lineElement: location.lineElement, + numberColumn: location.numberColumn, + clientX: event.clientX, + clientY: event.clientY, + }); + + lastDiffLocationRef.current = { + ...location, + column, + }; + }} + > + +
+
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.test.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.test.ts new file mode 100644 index 00000000000..98e928865ec --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "bun:test"; +import type { FileContents } from "shared/changes-types"; +import { mapDiffLocationToRawPosition } from "./diff-location"; + +describe("mapDiffLocationToRawPosition", () => { + it("keeps addition-side clicks on the same modified line", () => { + const contents: FileContents = { + original: "one\ntwo\nthree", + modified: "one\ntwo changed\nthree", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 2, + side: "additions", + lineType: "change-addition", + column: 5, + }), + ).toEqual({ + lineNumber: 2, + column: 5, + }); + }); + + it("maps deletion-side clicks to the replacement start line", () => { + const contents: FileContents = { + original: "one\ntwo\nthree", + modified: "one\ntwo changed\nthree", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 2, + side: "deletions", + lineType: "change-deletion", + }), + ).toEqual({ + lineNumber: 2, + column: 1, + }); + }); + + it("maps pure deletions to the insertion point in the modified file", () => { + const contents: FileContents = { + original: "one\ntwo\nthree\nfour", + modified: "one\nfour", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 2, + side: "deletions", + lineType: "change-deletion", + }), + ).toEqual({ + lineNumber: 2, + column: 1, + }); + }); + + it("maps deletion-side context lines using old-to-new line translation", () => { + const contents: FileContents = { + original: "one\ntwo\nthree\nfour", + modified: "zero\none\ntwo\nthree\nfour", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 4, + side: "deletions", + lineType: "context", + }), + ).toEqual({ + lineNumber: 5, + column: 1, + }); + }); + + it("maps old-side lines below the final hunk using the cumulative delta", () => { + const contents: FileContents = { + original: "one\ntwo\nthree\nfour\nfive", + modified: "zero\none\ntwo\nthree\nfour\nfive", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 5, + side: "deletions", + lineType: "context", + }), + ).toEqual({ + lineNumber: 6, + column: 1, + }); + }); + + it("clamps columns to the target line length", () => { + const contents: FileContents = { + original: "alpha", + modified: "beta", + language: "text", + }; + + expect( + mapDiffLocationToRawPosition({ + contents, + lineNumber: 1, + side: "additions", + lineType: "change-addition", + column: 99, + }), + ).toEqual({ + lineNumber: 1, + column: 5, + }); + }); +}); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.ts new file mode 100644 index 00000000000..d3d6c35ecc1 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/FileViewerContent/utils/diff-location.ts @@ -0,0 +1,299 @@ +import { parseDiffFromFile } from "@pierre/diffs"; +import type { AnnotationSide, LineTypes } from "@pierre/diffs/react"; +import type { FileContents } from "shared/changes-types"; + +interface MapDiffLocationToRawPositionOptions { + contents: FileContents; + lineNumber: number; + side: AnnotationSide; + lineType: LineTypes; + column?: number; +} + +interface DiffClickColumnOptions { + lineElement: HTMLElement; + numberColumn?: boolean; +} + +interface DiffPointColumnOptions extends DiffClickColumnOptions { + clientX: number; + clientY: number; +} + +export interface DiffDomLocation { + lineElement: HTMLElement; + lineNumber: number; + side: AnnotationSide; + lineType: LineTypes; + numberColumn: boolean; +} + +export interface RawEditorPosition { + lineNumber: number; + column: number; +} + +function isSupportedLineType(lineType: string): lineType is LineTypes { + return ( + lineType === "context" || + lineType === "context-expanded" || + lineType === "change-deletion" || + lineType === "change-addition" + ); +} + +function clampLineNumber(lineNumber: number, modifiedLines: string[]): number { + if (modifiedLines.length === 0) return 1; + return Math.max(1, Math.min(lineNumber, modifiedLines.length)); +} + +function clampColumn( + lineNumber: number, + column: number | undefined, + modifiedLines: string[], +): number { + const safeLineNumber = clampLineNumber(lineNumber, modifiedLines); + const lineContent = modifiedLines[safeLineNumber - 1] ?? ""; + const requestedColumn = column ?? 1; + + return Math.max(1, Math.min(requestedColumn, lineContent.length + 1)); +} + +function getDiffCodeElement(lineElement: HTMLElement): HTMLElement { + const codeElement = lineElement.querySelector("[data-code]"); + return codeElement instanceof HTMLElement ? codeElement : lineElement; +} + +let measurementCanvas: HTMLCanvasElement | null = null; + +function getMeasurementContext(): CanvasRenderingContext2D | null { + if (!measurementCanvas) { + measurementCanvas = document.createElement("canvas"); + } + + return measurementCanvas.getContext("2d"); +} + +function measureColumnFromRenderedText( + codeElement: HTMLElement, + clientX: number, +): number { + const lineText = codeElement.textContent ?? ""; + if (lineText.length === 0) { + return 1; + } + + const rect = codeElement.getBoundingClientRect(); + if (clientX <= rect.left) { + return 1; + } + + const style = window.getComputedStyle(codeElement); + const context = getMeasurementContext(); + if (!context) { + return 1; + } + + context.font = [ + style.fontStyle, + style.fontVariant, + style.fontWeight, + style.fontSize, + style.fontFamily, + ] + .filter(Boolean) + .join(" "); + + const tabSize = Number.parseInt(style.tabSize || "4", 10); + const safeTabSize = Number.isFinite(tabSize) && tabSize > 0 ? tabSize : 4; + const targetX = clientX - rect.left; + let previousWidth = 0; + let renderedText = ""; + const characters = Array.from(lineText); + + for (let index = 0; index < characters.length; index += 1) { + renderedText += + characters[index] === "\t" ? " ".repeat(safeTabSize) : characters[index]; + const nextWidth = context.measureText(renderedText).width; + const midpoint = previousWidth + (nextWidth - previousWidth) / 2; + + if (targetX <= midpoint) { + return index + 1; + } + + previousWidth = nextWidth; + } + + return characters.length + 1; +} + +function mapOldSideLineToRawLine( + contents: FileContents, + lineNumber: number, +): number { + const modifiedLines = contents.modified.split("\n"); + const diff = parseDiffFromFile( + { name: "before", contents: contents.original }, + { name: "after", contents: contents.modified }, + ); + let lineDelta = 0; + + for (const hunk of diff.hunks) { + if (lineNumber < hunk.deletionStart) { + return clampLineNumber(lineNumber + lineDelta, modifiedLines); + } + + let currentOldLine = hunk.deletionStart; + let currentNewLine = hunk.additionStart; + + for (const chunk of hunk.hunkContent) { + if (chunk.type === "context") { + for (let index = 0; index < chunk.lines.length; index += 1) { + if (currentOldLine === lineNumber) { + return clampLineNumber(currentNewLine, modifiedLines); + } + + currentOldLine += 1; + currentNewLine += 1; + } + continue; + } + + const insertionLine = clampLineNumber(currentNewLine, modifiedLines); + + for (let index = 0; index < chunk.deletions.length; index += 1) { + if (currentOldLine === lineNumber) { + return insertionLine; + } + currentOldLine += 1; + } + + currentNewLine += chunk.additions.length; + } + + lineDelta = currentNewLine - currentOldLine; + } + + return clampLineNumber(lineNumber + lineDelta, modifiedLines); +} + +export function mapDiffLocationToRawPosition({ + contents, + lineNumber, + side, + column, +}: MapDiffLocationToRawPositionOptions): RawEditorPosition { + const modifiedLines = contents.modified.split("\n"); + + const rawLineNumber = + side === "additions" + ? clampLineNumber(lineNumber, modifiedLines) + : mapOldSideLineToRawLine(contents, lineNumber); + + return { + lineNumber: rawLineNumber, + column: clampColumn(rawLineNumber, column, modifiedLines), + }; +} + +export function getColumnFromDiffPoint({ + lineElement, + clientX, + clientY, + numberColumn = false, +}: DiffPointColumnOptions): number { + if (numberColumn) { + return 1; + } + + const codeElement = getDiffCodeElement(lineElement); + const documentWithCaretApi = document as Document & { + caretPositionFromPoint?: ( + x: number, + y: number, + ) => { offsetNode: Node; offset: number } | null; + caretRangeFromPoint?: (x: number, y: number) => Range | null; + }; + const caretPosition = documentWithCaretApi.caretPositionFromPoint?.( + clientX, + clientY, + ); + if (caretPosition && codeElement.contains(caretPosition.offsetNode)) { + const measureRange = document.createRange(); + measureRange.selectNodeContents(codeElement); + measureRange.setEnd(caretPosition.offsetNode, caretPosition.offset); + return Math.max(1, measureRange.toString().length + 1); + } + + const caretRange = documentWithCaretApi.caretRangeFromPoint?.( + clientX, + clientY, + ); + if (caretRange && codeElement.contains(caretRange.startContainer)) { + const measureRange = document.createRange(); + measureRange.selectNodeContents(codeElement); + measureRange.setEnd(caretRange.startContainer, caretRange.startOffset); + return Math.max(1, measureRange.toString().length + 1); + } + + return measureColumnFromRenderedText(codeElement, clientX); +} + +export function getDiffLocationFromTarget( + target: EventTarget | null, +): DiffDomLocation | null { + if (!(target instanceof Node)) { + return null; + } + + const targetElement = + target instanceof HTMLElement ? target : target.parentElement; + const lineElement = targetElement?.closest("[data-line]"); + if (!(lineElement instanceof HTMLElement)) { + return null; + } + + const rawLineNumber = Number.parseInt(lineElement.dataset.line ?? "", 10); + const lineType = lineElement.dataset.lineType; + if ( + !Number.isFinite(rawLineNumber) || + !lineType || + !isSupportedLineType(lineType) + ) { + return null; + } + + const numberColumn = !!targetElement?.closest("[data-column-number]"); + const parentCode = lineElement.closest("[data-code]"); + const side: AnnotationSide = + lineType === "change-deletion" + ? "deletions" + : lineType === "change-addition" + ? "additions" + : parentCode instanceof HTMLElement && "deletions" in parentCode.dataset + ? "deletions" + : "additions"; + + return { + lineElement, + lineNumber: rawLineNumber, + side, + lineType, + numberColumn, + }; +} + +export function getDiffLocationFromEvent( + event: Pick, +): DiffDomLocation | null { + const composedPath = event.composedPath(); + + for (const pathEntry of composedPath) { + const location = getDiffLocationFromTarget(pathEntry ?? null); + if (location) { + return location; + } + } + + return getDiffLocationFromTarget(event.target); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx index 6a2429b0432..28773ca5835 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/EditorContextMenu/EditorContextMenu.tsx @@ -38,12 +38,14 @@ interface EditorContextMenuProps { children: ReactNode; editorActions: EditorActions; paneActions: PaneActions; + leadingItems?: ReactNode; } export function EditorContextMenu({ children, editorActions, paneActions, + leadingItems, }: EditorContextMenuProps) { const { data: platform } = electronTrpc.window.getPlatform.useQuery(); const isMac = platform === "darwin"; @@ -64,6 +66,13 @@ export function EditorContextMenu({ {children} + {leadingItems && ( + <> + {leadingItems} + + + )} + {/* Clipboard Actions */} {showCutPaste && ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts index e8bdd960b80..6fb832cb0c8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/constants.ts @@ -6,6 +6,7 @@ export const MIDNIGHT_CODE_COLORS = { background: "#282c34", border: "#21252b", muted: "#636d83", + activeLine: "#ffffff0a", selection: "#4b5668", search: "#e5c07b33", searchActive: "#e5c07b55", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts index 3f626371214..94e2b25a900 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/components/CodeEditor/createCodeMirrorTheme.ts @@ -44,10 +44,10 @@ export function createCodeMirrorTheme( borderRight: `1px solid ${MIDNIGHT_CODE_COLORS.border}`, }, ".cm-activeLine": { - backgroundColor: MIDNIGHT_CODE_COLORS.surface, + backgroundColor: MIDNIGHT_CODE_COLORS.activeLine, }, ".cm-activeLineGutter": { - backgroundColor: MIDNIGHT_CODE_COLORS.surface, + backgroundColor: MIDNIGHT_CODE_COLORS.activeLine, }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { diff --git a/apps/desktop/src/renderer/stores/tabs/store.ts b/apps/desktop/src/renderer/stores/tabs/store.ts index 003a92b83ed..11b560fc495 100644 --- a/apps/desktop/src/renderer/stores/tabs/store.ts +++ b/apps/desktop/src/renderer/stores/tabs/store.ts @@ -710,10 +710,14 @@ export const useTabsStore = create()( existingFileViewer.commitHash === options.commitHash; if (isSameFile) { - if ( - options.viewMode && - existingFileViewer.viewMode !== options.viewMode - ) { + const nextViewMode = + options.viewMode ?? existingFileViewer.viewMode; + const shouldUpdateViewerState = + nextViewMode !== existingFileViewer.viewMode || + options.line !== undefined || + options.column !== undefined; + + if (shouldUpdateViewerState) { set({ panes: { ...state.panes, @@ -721,7 +725,11 @@ export const useTabsStore = create()( ...paneToReuse, fileViewer: { ...existingFileViewer, - viewMode: options.viewMode, + viewMode: nextViewMode, + initialLine: + options.line ?? existingFileViewer.initialLine, + initialColumn: + options.column ?? existingFileViewer.initialColumn, }, }, },