blob: 1460dc36d26076d89ad14f3fe8bac6e6446ca794 [file] [log] [blame]
import * as Diff2Html from "diff2html";
/**
* Class to handle diff and commit viewing functionality in the timeline UI.
*/
export class DiffViewer {
// Current commit hash being viewed
private currentCommitHash: string = "";
// Selected line in the diff for commenting
private selectedDiffLine: string | null = null;
// Current view mode (needed for integration with TimelineManager)
private viewMode: string = "chat";
/**
* Constructor for DiffViewer
*/
constructor() {}
/**
* Sets the current view mode
* @param mode The current view mode
*/
public setViewMode(mode: string): void {
this.viewMode = mode;
}
/**
* Gets the current commit hash
* @returns The current commit hash
*/
public getCurrentCommitHash(): string {
return this.currentCommitHash;
}
/**
* Sets the current commit hash
* @param hash The commit hash to set
*/
public setCurrentCommitHash(hash: string): void {
this.currentCommitHash = hash;
}
/**
* Clears the current commit hash
*/
public clearCurrentCommitHash(): void {
this.currentCommitHash = "";
}
/**
* Loads diff content and renders it using diff2html
* @param commitHash Optional commit hash to load diff for
*/
public async loadDiff2HtmlContent(commitHash?: string): Promise<void> {
const diff2htmlContent = document.getElementById("diff2htmlContent");
const container = document.querySelector(".timeline-container");
if (!diff2htmlContent || !container) return;
try {
// Show loading state
diff2htmlContent.innerHTML = "Loading enhanced diff...";
// Add classes to container to allow full-width rendering
container.classList.add("diff2-active");
container.classList.add("diff-active");
// Use currentCommitHash if provided or passed from parameter
const hash = commitHash || this.currentCommitHash;
// Build the diff URL - include commit hash if specified
const diffUrl = hash ? `diff?commit=${hash}` : "diff";
// Fetch the diff from the server
const response = await fetch(diffUrl);
if (!response.ok) {
throw new Error(
`Server returned ${response.status}: ${response.statusText}`,
);
}
const diffText = await response.text();
if (!diffText || diffText.trim() === "") {
diff2htmlContent.innerHTML =
"<span style='color: #666; font-style: italic;'>No changes detected since conversation started.</span>";
return;
}
// Get the selected view format
const formatRadios = document.getElementsByName("diffViewFormat") as NodeListOf<HTMLInputElement>;
let outputFormat = "side-by-side"; // default
// Convert NodeListOf to Array to ensure [Symbol.iterator]() is available
Array.from(formatRadios).forEach(radio => {
if (radio.checked) {
outputFormat = radio.value as "side-by-side" | "line-by-line";
}
})
// Render the diff using diff2html
const diffHtml = Diff2Html.html(diffText, {
outputFormat: outputFormat as "side-by-side" | "line-by-line",
drawFileList: true,
matching: "lines",
// Make sure no unnecessary scrollbars in the nested containers
renderNothingWhenEmpty: false,
colorScheme: "light" as any, // Force light mode to match the rest of the UI
});
// Insert the generated HTML
diff2htmlContent.innerHTML = diffHtml;
// Add CSS styles to ensure we don't have double scrollbars
const d2hFiles = diff2htmlContent.querySelectorAll(".d2h-file-wrapper");
d2hFiles.forEach((file) => {
const contentElem = file.querySelector(".d2h-files-diff");
if (contentElem) {
// Remove internal scrollbar - the outer container will handle scrolling
(contentElem as HTMLElement).style.overflow = "visible";
(contentElem as HTMLElement).style.maxHeight = "none";
}
});
// Add click event handlers to each code line for commenting
this.setupDiff2LineComments();
// Setup event listeners for diff view format radio buttons
this.setupDiffViewFormatListeners();
} catch (error) {
console.error("Error loading diff2html content:", error);
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
diff2htmlContent.innerHTML = `<span style='color: #dc3545;'>Error loading enhanced diff: ${errorMessage}</span>`;
}
}
/**
* Setup event listeners for diff view format radio buttons
*/
private setupDiffViewFormatListeners(): void {
const formatRadios = document.getElementsByName("diffViewFormat") as NodeListOf<HTMLInputElement>;
// Convert NodeListOf to Array to ensure [Symbol.iterator]() is available
Array.from(formatRadios).forEach(radio => {
radio.addEventListener("change", () => {
// Reload the diff with the new format when radio selection changes
this.loadDiff2HtmlContent(this.currentCommitHash);
});
})
}
/**
* Setup handlers for diff2 code lines to enable commenting
*/
private setupDiff2LineComments(): void {
const diff2htmlContent = document.getElementById("diff2htmlContent");
if (!diff2htmlContent) return;
console.log("Setting up diff2 line comments");
// Add plus buttons to each code line
this.addCommentButtonsToCodeLines();
// Use event delegation for handling clicks on plus buttons
diff2htmlContent.addEventListener("click", (event) => {
const target = event.target as HTMLElement;
// Only respond to clicks on the plus button
if (target.classList.contains("d2h-gutter-comment-button")) {
// Find the parent row first
const row = target.closest("tr");
if (!row) return;
// Then find the code line in that row
const codeLine = row.querySelector(".d2h-code-side-line") || row.querySelector(".d2h-code-line");
if (!codeLine) return;
// Get the line text content
const lineContent = codeLine.querySelector(".d2h-code-line-ctn");
if (!lineContent) return;
const lineText = lineContent.textContent?.trim() || "";
// Get file name to add context
const fileHeader = codeLine
.closest(".d2h-file-wrapper")
?.querySelector(".d2h-file-name");
const fileName = fileHeader
? fileHeader.textContent?.trim()
: "Unknown file";
// Get line number if available
const lineNumElem = codeLine
.closest("tr")
?.querySelector(".d2h-code-side-linenumber");
const lineNum = lineNumElem ? lineNumElem.textContent?.trim() : "";
const lineInfo = lineNum ? `Line ${lineNum}: ` : "";
// Format the line for the comment box with file context and line number
const formattedLine = `${fileName} ${lineInfo}${lineText}`;
console.log("Comment button clicked for line: ", formattedLine);
// Open the comment box with this line
this.openDiffCommentBox(formattedLine, 0);
// Prevent event from bubbling up
event.stopPropagation();
}
});
// Handle text selection
let isSelecting = false;
diff2htmlContent.addEventListener("mousedown", () => {
isSelecting = false;
});
diff2htmlContent.addEventListener("mousemove", (event) => {
// If mouse is moving with button pressed, user is selecting text
if (event.buttons === 1) { // Primary button (usually left) is pressed
isSelecting = true;
}
});
}
/**
* Add plus buttons to each table row in the diff for commenting
*/
private addCommentButtonsToCodeLines(): void {
const diff2htmlContent = document.getElementById("diff2htmlContent");
if (!diff2htmlContent) return;
// Target code lines first, then find their parent rows
const codeLines = diff2htmlContent.querySelectorAll(
".d2h-code-side-line, .d2h-code-line"
);
// Create a Set to store unique rows to avoid duplicates
const rowsSet = new Set<HTMLElement>();
// Get all rows that contain code lines
codeLines.forEach(line => {
const row = line.closest('tr');
if (row) rowsSet.add(row as HTMLElement);
});
// Convert Set back to array for processing
const codeRows = Array.from(rowsSet);
codeRows.forEach((row) => {
const rowElem = row as HTMLElement;
// Skip info lines without actual code (e.g., "file added")
if (rowElem.querySelector(".d2h-info")) {
return;
}
// Find the code line number element (first TD in the row)
const lineNumberCell = rowElem.querySelector(
".d2h-code-side-linenumber, .d2h-code-linenumber"
);
if (!lineNumberCell) return;
// Create the plus button
const plusButton = document.createElement("span");
plusButton.className = "d2h-gutter-comment-button";
plusButton.innerHTML = "+";
plusButton.title = "Add a comment on this line";
// Add button to the line number cell for proper positioning
(lineNumberCell as HTMLElement).style.position = "relative"; // Ensure positioning context
lineNumberCell.appendChild(plusButton);
});
}
/**
* Open the comment box for a selected diff line
*/
private openDiffCommentBox(lineText: string, _lineNumber: number): void {
const commentBox = document.getElementById("diffCommentBox");
const selectedLine = document.getElementById("selectedLine");
const commentInput = document.getElementById(
"diffCommentInput",
) as HTMLTextAreaElement;
if (!commentBox || !selectedLine || !commentInput) return;
// Store the selected line
this.selectedDiffLine = lineText;
// Display the line in the comment box
selectedLine.textContent = lineText;
// Reset the comment input
commentInput.value = "";
// Show the comment box
commentBox.style.display = "block";
// Focus on the comment input
commentInput.focus();
// Add event listeners for submit and cancel buttons
const submitButton = document.getElementById("submitDiffComment");
if (submitButton) {
submitButton.onclick = () => this.submitDiffComment();
}
const cancelButton = document.getElementById("cancelDiffComment");
if (cancelButton) {
cancelButton.onclick = () => this.closeDiffCommentBox();
}
}
/**
* Close the diff comment box without submitting
*/
private closeDiffCommentBox(): void {
const commentBox = document.getElementById("diffCommentBox");
if (commentBox) {
commentBox.style.display = "none";
}
this.selectedDiffLine = null;
}
/**
* Submit a comment on a diff line
*/
private submitDiffComment(): void {
const commentInput = document.getElementById(
"diffCommentInput",
) as HTMLTextAreaElement;
const chatInput = document.getElementById(
"chatInput",
) as HTMLTextAreaElement;
if (!commentInput || !chatInput) return;
const comment = commentInput.value.trim();
// Validate inputs
if (!this.selectedDiffLine || !comment) {
alert("Please select a line and enter a comment.");
return;
}
// Format the comment in a readable way
const formattedComment = `\`\`\`\n${this.selectedDiffLine}\n\`\`\`\n\n${comment}`;
// Append the formatted comment to the chat textarea
if (chatInput.value.trim() !== "") {
chatInput.value += "\n\n"; // Add two line breaks before the new comment
}
chatInput.value += formattedComment;
chatInput.focus();
// Close only the comment box but keep the diff view open
this.closeDiffCommentBox();
}
/**
* Show diff for a specific commit
* @param commitHash The commit hash to show diff for
* @param toggleViewModeCallback Callback to toggle view mode to diff
*/
public showCommitDiff(commitHash: string, toggleViewModeCallback: (mode: string) => void): void {
// Store the commit hash
this.currentCommitHash = commitHash;
// Switch to diff2 view (side-by-side)
toggleViewModeCallback("diff2");
}
/**
* Clean up resources when component is destroyed
*/
public dispose(): void {
// Clean up any resources or event listeners here
// Currently there are no specific resources to clean up
}
}