blob: 03aaa3f530037c42477971f1ce45abf2ac82abc6 [file] [log] [blame]
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { SketchTailwindElement } from "./sketch-tailwind-element.js";
import "./sketch-monaco-view";
import "./sketch-diff-range-picker";
import "./sketch-diff-empty-view";
import { GitDiffFile, GitDataService } from "./git-data-service";
import { DiffRange } from "./sketch-diff-range-picker";
/**
* A component that displays diffs using Monaco editor with range and file pickers
*/
@customElement("sketch-diff2-view")
export class SketchDiff2View extends SketchTailwindElement {
/**
* Handles comment events from the Monaco editor and forwards them to the chat input
* using the same event format as the original diff view for consistency.
*/
private handleMonacoComment(event: CustomEvent) {
try {
// Validate incoming data
if (!event.detail || !event.detail.formattedComment) {
console.error("Invalid comment data received");
return;
}
// Create and dispatch event using the standardized format
const commentEvent = new CustomEvent("diff-comment", {
detail: { comment: event.detail.formattedComment },
bubbles: true,
composed: true,
});
this.dispatchEvent(commentEvent);
} catch (error) {
console.error("Error handling Monaco comment:", error);
}
}
/**
* Handle height change events from the Monaco editor
*/
private handleMonacoHeightChange(event: CustomEvent) {
try {
// Get the monaco view that emitted the event
const monacoView = event.target as HTMLElement;
if (!monacoView) return;
// Find the parent Monaco editor container (now using Tailwind classes)
const fileDiffEditor = monacoView.closest(
".flex.flex-col.w-full.min-h-\\[200px\\].flex-1",
) as HTMLElement;
if (!fileDiffEditor) return;
// Get the new height from the event
const newHeight = event.detail.height;
// Only update if the height actually changed to avoid unnecessary layout
const currentHeight = fileDiffEditor.style.height;
const newHeightStr = `${newHeight}px`;
if (currentHeight !== newHeightStr) {
// Update the container height to match monaco's height
fileDiffEditor.style.height = newHeightStr;
// Remove any previous min-height constraint that might interfere
fileDiffEditor.style.minHeight = "auto";
// IMPORTANT: Tell Monaco to relayout after its container size changed
// Monaco has automaticLayout: false, so it won't detect container changes
setTimeout(() => {
const monacoComponent = monacoView as any;
if (monacoComponent && monacoComponent.editor) {
// Force layout with explicit dimensions to ensure Monaco fills the space
const editorWidth = fileDiffEditor.offsetWidth;
monacoComponent.editor.layout({
width: editorWidth,
height: newHeight,
});
}
}, 0);
}
} catch (error) {
console.error("Error handling Monaco height change:", error);
}
}
/**
* Handle save events from the Monaco editor
*/
private async handleMonacoSave(event: CustomEvent) {
try {
// Validate incoming data
if (
!event.detail ||
!event.detail.path ||
event.detail.content === undefined
) {
console.error("Invalid save data received");
return;
}
const { path, content } = event.detail;
// Get Monaco view component
const monacoView = this.querySelector("sketch-monaco-view");
if (!monacoView) {
console.error("Monaco view not found");
return;
}
try {
await this.gitService?.saveFileContent(path, content);
console.log(`File saved: ${path}`);
(monacoView as any).notifySaveComplete(true);
} catch (error) {
console.error(
`Error saving file: ${error instanceof Error ? error.message : String(error)}`,
);
(monacoView as any).notifySaveComplete(false);
}
} catch (error) {
console.error("Error handling save:", error);
}
}
@property({ type: String })
initialCommit: string = "";
// The commit to show - used when showing a specific commit from timeline
@property({ type: String })
commit: string = "";
@property({ type: String })
selectedFilePath: string = "";
@state()
private files: GitDiffFile[] = [];
@state()
private currentRange: DiffRange = { type: "range", from: "", to: "HEAD" };
@state()
private fileContents: Map<
string,
{ original: string; modified: string; editable: boolean }
> = new Map();
@state()
private fileExpandStates: Map<string, boolean> = new Map();
@state()
private loading: boolean = false;
@state()
private error: string | null = null;
@state()
private selectedFile: string = ""; // Empty string means "All files"
@state()
private viewMode: "all" | "single" = "all";
@state()
private untrackedFiles: string[] = [];
@state()
private showUntrackedPopup: boolean = false;
@property({ attribute: false, type: Object })
gitService!: GitDataService;
// The gitService must be passed from parent to ensure proper dependency injection
constructor() {
super();
console.log("SketchDiff2View initialized");
// Fix for monaco-aria-container positioning and hide scrollbars globally
// Add a global style to ensure proper positioning of aria containers and hide scrollbars
const styleElement = document.createElement("style");
styleElement.textContent = `
.monaco-aria-container {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(1px, 1px, 1px, 1px) !important;
white-space: nowrap !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
z-index: -1 !important;
}
/* Aggressively hide all Monaco scrollbar elements */
.monaco-editor .scrollbar,
.monaco-editor .scroll-decoration,
.monaco-editor .invisible.scrollbar,
.monaco-editor .slider,
.monaco-editor .vertical.scrollbar,
.monaco-editor .horizontal.scrollbar,
.monaco-diff-editor .scrollbar,
.monaco-diff-editor .scroll-decoration,
.monaco-diff-editor .invisible.scrollbar,
.monaco-diff-editor .slider,
.monaco-diff-editor .vertical.scrollbar,
.monaco-diff-editor .horizontal.scrollbar {
display: none !important;
visibility: hidden !important;
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
}
/* Target the specific scrollbar classes that Monaco uses */
.monaco-scrollable-element > .scrollbar,
.monaco-scrollable-element > .scroll-decoration,
.monaco-scrollable-element .slider {
display: none !important;
visibility: hidden !important;
width: 0 !important;
height: 0 !important;
}
/* Remove scrollbar space/padding from content area */
.monaco-editor .monaco-scrollable-element,
.monaco-diff-editor .monaco-scrollable-element {
padding-right: 0 !important;
padding-bottom: 0 !important;
margin-right: 0 !important;
margin-bottom: 0 !important;
}
/* Ensure the diff content takes full width without scrollbar space */
.monaco-diff-editor .editor.modified,
.monaco-diff-editor .editor.original {
margin-right: 0 !important;
padding-right: 0 !important;
}
`;
document.head.appendChild(styleElement);
}
// Override createRenderRoot to apply host styles for proper sizing while still using light DOM
createRenderRoot() {
// Use light DOM like SketchTailwindElement but still apply host styles
const style = document.createElement("style");
style.textContent = `
sketch-diff2-view {
height: -webkit-fill-available;
}
`;
// Add the style to the document head if not already present
if (!document.head.querySelector("style[data-sketch-diff2-view]")) {
style.setAttribute("data-sketch-diff2-view", "");
document.head.appendChild(style);
}
return this;
}
connectedCallback() {
super.connectedCallback();
// Initialize with default range and load data
// Get base commit if not set
if (
this.currentRange.type === "range" &&
!("from" in this.currentRange && this.currentRange.from)
) {
this.gitService
.getBaseCommitRef()
.then((baseRef) => {
this.currentRange = { type: "range", from: baseRef, to: "HEAD" };
this.loadDiffData();
})
.catch((error) => {
console.error("Error getting base commit ref:", error);
// Use default range
this.loadDiffData();
});
} else {
this.loadDiffData();
}
// Add click listener to close popup when clicking outside
document.addEventListener("click", this.handleDocumentClick.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("click", this.handleDocumentClick.bind(this));
}
handleDocumentClick(event: Event) {
if (this.showUntrackedPopup) {
const target = event.target as HTMLElement;
// Check if click is outside the popup and button
if (!target.closest(".relative")) {
this.showUntrackedPopup = false;
}
}
}
// Toggle hideUnchangedRegions setting for a specific file
private toggleFileExpansion(filePath: string) {
const currentState = this.fileExpandStates.get(filePath) ?? false;
const newState = !currentState;
this.fileExpandStates.set(filePath, newState);
// Apply to the specific Monaco view component for this file
const monacoView = this.querySelector(
`sketch-monaco-view[data-file-path="${filePath}"]`,
);
if (monacoView) {
(monacoView as any).toggleHideUnchangedRegions(!newState); // inverted because true means "hide unchanged"
}
// Force a re-render to update the button state
this.requestUpdate();
}
render() {
return html`
<div class="px-4 py-2 border-b border-gray-300 bg-gray-100 flex-shrink-0">
<div class="flex flex-col gap-3">
<div class="w-full flex items-center gap-3">
<sketch-diff-range-picker
class="flex-1 min-w-[400px]"
.gitService="${this.gitService}"
@range-change="${this.handleRangeChange}"
></sketch-diff-range-picker>
${this.renderUntrackedFilesNotification()}
<div class="flex-1"></div>
${this.renderFileSelector()}
</div>
</div>
</div>
<div class="flex-1 overflow-auto flex flex-col min-h-0 relative h-full">
${this.renderDiffContent()}
</div>
`;
}
renderFileSelector() {
const fileCount = this.files.length;
return html`
<div class="flex items-center gap-2">
<select
class="min-w-[200px] px-3 py-2 border border-gray-400 rounded bg-white text-sm cursor-pointer focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed"
.value="${this.selectedFile}"
@change="${this.handleFileSelection}"
?disabled="${fileCount === 0}"
>
<option value="">All files (${fileCount})</option>
${this.files.map(
(file) => html`
<option value="${file.path}">
${this.getFileDisplayName(file)}
</option>
`,
)}
</select>
${this.selectedFile ? this.renderSingleFileExpandButton() : ""}
</div>
`;
}
renderUntrackedFilesNotification() {
if (!this.untrackedFiles || this.untrackedFiles.length === 0) {
return "";
}
const fileCount = this.untrackedFiles.length;
const fileCountText =
fileCount === 1 ? "1 untracked file" : `${fileCount} untracked files`;
return html`
<div class="relative">
<button
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-gray-100 text-gray-700 border border-gray-300 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
@click="${this.toggleUntrackedFilesPopup}"
type="button"
>
${fileCount} untracked
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
${this.showUntrackedPopup
? html`
<div
class="absolute top-full left-0 mt-2 w-80 bg-white border border-gray-300 rounded-lg shadow-lg z-50"
>
<div class="p-4">
<div class="flex items-start gap-3 mb-3">
<svg
class="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div class="flex-1">
<div class="font-medium text-gray-900 mb-1">
${fileCountText}
</div>
<div class="text-sm text-gray-600 mb-3">
These files are not tracked by git. They will be lost if
the session ends now. The agent typically does not add
files to git until it is ready for feedback.
</div>
</div>
</div>
<div class="max-h-32 overflow-y-auto">
<div class="text-sm text-gray-700">
${this.untrackedFiles.map(
(file) => html`
<div
class="py-1 px-2 hover:bg-gray-100 rounded font-mono text-xs"
>
${file}
</div>
`,
)}
</div>
</div>
</div>
</div>
`
: ""}
</div>
`;
}
renderDiffContent() {
if (this.loading) {
return html`<div class="flex items-center justify-center h-full">
Loading diff...
</div>`;
}
if (this.error) {
return html`<div class="text-red-600 p-4">${this.error}</div>`;
}
if (this.files.length === 0) {
return html`<sketch-diff-empty-view></sketch-diff-empty-view>`;
}
// Render single file view if a specific file is selected
if (this.selectedFile && this.viewMode === "single") {
return this.renderSingleFileView();
}
// Render multi-file view
return html`
<div class="flex flex-col w-full min-h-full">
${this.files.map((file, index) => this.renderFileDiff(file, index))}
</div>
`;
}
/**
* Load diff data for the current range
*/
async loadDiffData() {
this.loading = true;
this.error = null;
try {
// Initialize files as empty array if undefined
if (!this.files) {
this.files = [];
}
// Load diff data for the range
this.files = await this.gitService.getDiff(
this.currentRange.from,
this.currentRange.to,
);
// Ensure files is always an array, even when API returns null
if (!this.files) {
this.files = [];
}
// Load untracked files for notification
try {
this.untrackedFiles = await this.gitService.getUntrackedFiles();
} catch (error) {
console.error("Error loading untracked files:", error);
this.untrackedFiles = [];
}
// Load content for all files
if (this.files.length > 0) {
// Initialize expand states for new files (default to collapsed)
this.files.forEach((file) => {
if (!this.fileExpandStates.has(file.path)) {
this.fileExpandStates.set(file.path, false); // false = collapsed (hide unchanged regions)
}
});
await this.loadAllFileContents();
} else {
// No files to display - reset the view to initial state
this.selectedFilePath = "";
this.selectedFile = "";
this.viewMode = "all";
this.fileContents.clear();
this.fileExpandStates.clear();
}
} catch (error) {
console.error("Error loading diff data:", error);
this.error = `Error loading diff data: ${error.message}`;
// Ensure files is an empty array when an error occurs
this.files = [];
// Reset the view to initial state
this.selectedFilePath = "";
this.selectedFile = "";
this.viewMode = "all";
this.fileContents.clear();
this.fileExpandStates.clear();
} finally {
this.loading = false;
// Request a re-render.
// Empirically, without this line, diffs are visibly slow to load.
this.requestUpdate();
}
}
/**
* Load content for all files in the diff
*/
async loadAllFileContents() {
this.loading = true;
this.error = null;
this.fileContents.clear();
try {
let isUnstagedChanges = false;
// Determine the commits to compare based on the current range
const _fromCommit = this.currentRange.from;
const toCommit = this.currentRange.to;
// Check if this is an unstaged changes view
isUnstagedChanges = toCommit === "";
// Load content for all files
const promises = this.files.map(async (file) => {
try {
let originalCode = "";
let modifiedCode = "";
let editable = isUnstagedChanges;
// Load the original code based on file status
if (file.status !== "A") {
// For modified, renamed, or deleted files: load original content
originalCode = await this.gitService.getFileContent(
file.old_hash || "",
);
}
// For modified code, always use working copy when editable
if (editable) {
try {
// Always use working copy when editable, regardless of diff status
modifiedCode = await this.gitService.getWorkingCopyContent(
file.path,
);
} catch (error) {
if (file.status === "D") {
// For deleted files, silently use empty content
console.warn(
`Could not get working copy for deleted file ${file.path}, using empty content`,
);
modifiedCode = "";
} else {
// For any other file status, propagate the error
console.error(
`Failed to get working copy for ${file.path}:`,
error,
);
throw error;
}
}
} else {
// For non-editable view, use git content based on file status
if (file.status === "D") {
// Deleted file: empty modified
modifiedCode = "";
} else {
// Added/modified/renamed: use the content from git
modifiedCode = await this.gitService.getFileContent(
file.new_hash || "",
);
}
}
// Don't make deleted files editable
if (file.status === "D") {
editable = false;
}
this.fileContents.set(file.path, {
original: originalCode,
modified: modifiedCode,
editable,
});
} catch (error) {
console.error(`Error loading content for file ${file.path}:`, error);
// Store empty content for failed files to prevent blocking
this.fileContents.set(file.path, {
original: "",
modified: "",
editable: false,
});
}
});
await Promise.all(promises);
} catch (error) {
console.error("Error loading file contents:", error);
this.error = `Error loading file contents: ${error.message}`;
} finally {
this.loading = false;
}
}
/**
* Handle range change event from the range picker
*/
handleRangeChange(event: CustomEvent) {
const { range } = event.detail;
console.log("Range changed:", range);
this.currentRange = range;
// Load diff data for the new range
this.loadDiffData();
}
/**
* Render a single file diff section
*/
renderFileDiff(file: GitDiffFile, index: number) {
const content = this.fileContents.get(file.path);
if (!content) {
return html`
<div
class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
>
<div
class="bg-gray-100 border-b border-gray-300 px-4 py-2 font-medium text-sm text-gray-800 sticky top-0 z-10 shadow-sm flex justify-between items-center"
>
${this.renderFileHeader(file)}
</div>
<div class="flex items-center justify-center h-full">
Loading ${file.path}...
</div>
</div>
`;
}
return html`
<div
class="flex flex-col border-b-4 border-gray-300 mb-0 last:border-b-0"
>
<div
class="bg-gray-100 border-b border-gray-300 px-4 py-2 font-medium text-sm text-gray-800 sticky top-0 z-10 shadow-sm flex justify-between items-center"
>
${this.renderFileHeader(file)}
</div>
<div class="flex flex-col w-full min-h-[200px] flex-1">
<sketch-monaco-view
class="flex flex-col w-full min-h-[200px] flex-1"
.originalCode="${content.original}"
.modifiedCode="${content.modified}"
.originalFilename="${file.path}"
.modifiedFilename="${file.path}"
?readOnly="${!content.editable}"
?editable-right="${content.editable}"
@monaco-comment="${this.handleMonacoComment}"
@monaco-save="${this.handleMonacoSave}"
@monaco-height-changed="${this.handleMonacoHeightChange}"
data-file-index="${index}"
data-file-path="${file.path}"
></sketch-monaco-view>
</div>
</div>
`;
}
/**
* Render file header with status and path info
*/
renderFileHeader(file: GitDiffFile) {
const statusClasses = this.getFileStatusTailwindClasses(file.status);
const statusText = this.getFileStatusText(file.status);
const changesInfo = this.getChangesInfo(file);
const pathInfo = this.getPathInfo(file);
const isExpanded = this.fileExpandStates.get(file.path) ?? false;
return html`
<div class="flex items-center gap-2">
<span
class="inline-block px-1.5 py-0.5 rounded text-xs font-bold mr-2 ${statusClasses}"
>
${statusText}
</span>
<span class="font-mono font-normal text-gray-600">${pathInfo}</span>
${changesInfo
? html`<span class="ml-2 text-xs text-gray-600">${changesInfo}</span>`
: ""}
</div>
<div class="flex items-center">
<button
class="bg-transparent border border-gray-300 rounded px-2 py-1 text-sm cursor-pointer whitespace-nowrap transition-colors duration-200 flex items-center justify-center min-w-8 min-h-8 hover:bg-gray-200"
@click="${() => this.toggleFileExpansion(file.path)}"
title="${isExpanded
? "Collapse: Hide unchanged regions to focus on changes"
: "Expand: Show all lines including unchanged regions"}"
>
${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
</button>
</div>
`;
}
/**
* Get Tailwind CSS classes for file status
*/
getFileStatusTailwindClasses(status: string): string {
switch (status.toUpperCase()) {
case "A":
return "bg-green-100 text-green-800";
case "M":
return "bg-yellow-100 text-yellow-800";
case "D":
return "bg-red-100 text-red-800";
case "R":
case "C":
default:
if (status.toUpperCase().startsWith("R")) {
return "bg-cyan-100 text-cyan-800";
}
if (status.toUpperCase().startsWith("C")) {
return "bg-indigo-100 text-indigo-800";
}
return "bg-yellow-100 text-yellow-800";
}
}
/**
* Get display text for file status
*/
getFileStatusText(status: string): string {
switch (status.toUpperCase()) {
case "A":
return "Added";
case "M":
return "Modified";
case "D":
return "Deleted";
case "R":
case "C":
default:
if (status.toUpperCase().startsWith("R")) {
return "Renamed";
}
if (status.toUpperCase().startsWith("C")) {
return "Copied";
}
return "Modified";
}
}
/**
* Get changes information (+/-) for display
*/
getChangesInfo(file: GitDiffFile): string {
const additions = file.additions || 0;
const deletions = file.deletions || 0;
if (additions === 0 && deletions === 0) {
return "";
}
const parts = [];
if (additions > 0) {
parts.push(`+${additions}`);
}
if (deletions > 0) {
parts.push(`-${deletions}`);
}
return `(${parts.join(", ")})`;
}
/**
* Get path information for display, handling renames
*/
getPathInfo(file: GitDiffFile): string {
if (file.old_path && file.old_path !== "") {
// For renames, show old_path → new_path
return `${file.old_path} → ${file.path}`;
}
// For regular files, just show the path
return file.path;
}
/**
* Render expand all icon (dotted line with arrows pointing away)
*/
renderExpandAllIcon() {
return html`
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<!-- Dotted line in the middle -->
<line
x1="2"
y1="8"
x2="14"
y2="8"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="2,1"
/>
<!-- Large arrow pointing up -->
<path d="M8 2 L5 6 L11 6 Z" fill="currentColor" />
<!-- Large arrow pointing down -->
<path d="M8 14 L5 10 L11 10 Z" fill="currentColor" />
</svg>
`;
}
/**
* Render collapse icon (arrows pointing towards dotted line)
*/
renderCollapseIcon() {
return html`
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<!-- Dotted line in the middle -->
<line
x1="2"
y1="8"
x2="14"
y2="8"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="2,1"
/>
<!-- Large arrow pointing down towards line -->
<path d="M8 6 L5 2 L11 2 Z" fill="currentColor" />
<!-- Large arrow pointing up towards line -->
<path d="M8 10 L5 14 L11 14 Z" fill="currentColor" />
</svg>
`;
}
/**
* Handle file selection change from the dropdown
*/
handleFileSelection(event: Event) {
const selectElement = event.target as HTMLSelectElement;
const selectedValue = selectElement.value;
this.selectedFile = selectedValue;
this.viewMode = selectedValue ? "single" : "all";
// Force re-render
this.requestUpdate();
}
toggleUntrackedFilesPopup() {
this.showUntrackedPopup = !this.showUntrackedPopup;
}
/**
* Get display name for file in the selector
*/
getFileDisplayName(file: GitDiffFile): string {
const status = this.getFileStatusText(file.status);
const pathInfo = this.getPathInfo(file);
return `${status}: ${pathInfo}`;
}
/**
* Render expand/collapse button for single file view in header
*/
renderSingleFileExpandButton() {
if (!this.selectedFile) return "";
const isExpanded = this.fileExpandStates.get(this.selectedFile) ?? false;
return html`
<button
class="bg-transparent border border-gray-300 rounded px-1.5 py-1.5 text-sm cursor-pointer whitespace-nowrap transition-colors duration-200 flex items-center justify-center min-w-8 min-h-8 hover:bg-gray-200"
@click="${() => this.toggleFileExpansion(this.selectedFile)}"
title="${isExpanded
? "Collapse: Hide unchanged regions to focus on changes"
: "Expand: Show all lines including unchanged regions"}"
>
${isExpanded ? this.renderCollapseIcon() : this.renderExpandAllIcon()}
</button>
`;
}
/**
* Render single file view with full-screen Monaco editor
*/
renderSingleFileView() {
const selectedFileData = this.files.find(
(f) => f.path === this.selectedFile,
);
if (!selectedFileData) {
return html`<div class="text-red-600 p-4">Selected file not found</div>`;
}
const content = this.fileContents.get(this.selectedFile);
if (!content) {
return html`<div class="flex items-center justify-center h-full">
Loading ${this.selectedFile}...
</div>`;
}
return html`
<div class="flex-1 flex flex-col h-full min-h-0">
<sketch-monaco-view
class="flex-1 w-full h-full min-h-0"
.originalCode="${content.original}"
.modifiedCode="${content.modified}"
.originalFilename="${selectedFileData.path}"
.modifiedFilename="${selectedFileData.path}"
?readOnly="${!content.editable}"
?editable-right="${content.editable}"
@monaco-comment="${this.handleMonacoComment}"
@monaco-save="${this.handleMonacoSave}"
data-file-path="${selectedFileData.path}"
></sketch-monaco-view>
</div>
`;
}
/**
* Refresh the diff view by reloading commits and diff data
*
* This is called when the Monaco diff tab is activated to ensure:
* 1. Branch information from git/recentlog is current (branches can change frequently)
* 2. The diff content is synchronized with the latest repository state
* 3. Users always see up-to-date information without manual refresh
*/
refreshDiffView() {
// First refresh the range picker to get updated branch information
const rangePicker = this.querySelector("sketch-diff-range-picker");
if (rangePicker) {
(rangePicker as any).loadCommits();
}
if (this.commit) {
// Convert single commit to range (commit^ to commit)
this.currentRange = {
type: "range",
from: `${this.commit}^`,
to: this.commit,
};
}
// Then reload diff data based on the current range
this.loadDiffData();
}
}
declare global {
interface HTMLElementTagNameMap {
"sketch-diff2-view": SketchDiff2View;
}
}