diff --git a/packages/@sparrow-workspaces/src/features/rest-explorer/components/request-tests/sub-components/script/Script.svelte b/packages/@sparrow-workspaces/src/features/rest-explorer/components/request-tests/sub-components/script/Script.svelte index 0703094773..d609585015 100644 --- a/packages/@sparrow-workspaces/src/features/rest-explorer/components/request-tests/sub-components/script/Script.svelte +++ b/packages/@sparrow-workspaces/src/features/rest-explorer/components/request-tests/sub-components/script/Script.svelte @@ -62,6 +62,8 @@ let originalTestContent = ""; let currentPrompt = ""; let originalLineCount = 0; + let temporaryDisplayContent = ""; + let contentAddedDuringGeneration = ""; // Persistent highlighting variables let observer: MutationObserver | null = null; @@ -85,35 +87,175 @@ const updateBeautifiedState = (val: boolean): void => { isBodyBeautified = val; }; + //handler function to remove the changes on tab switch export const handleTabChange = () => { if (showGeneratedTestActions) { rejectGeneratedTest(); } }; - const handleCodeMirrorChange = (e: any) => { - onTestsChange({ ...tests, script: e.detail }); + const newContent = e.detail; + + // If we have generated test actions showing, protect the generated content + if (showGeneratedTestActions) { + const newLines = newContent.split("\n"); + + // Check if lines in the generated range have been modified + let generatedContentModified = false; + const originalGeneratedLines = generatedTestContent.split("\n"); + + for (let i = 0; i < originalGeneratedLines.length; i++) { + const lineIndex = generatedContentStartLine + i; + if (lineIndex < newLines.length) { + if (newLines[lineIndex] !== originalGeneratedLines[i]) { + generatedContentModified = true; + break; + } + } else { + // Generated content was deleted + generatedContentModified = true; + break; + } + } + + if (generatedContentModified) { + // Generated content was modified - block the change + tick().then(() => { + // Force revert by not updating temporaryDisplayContent + temporaryDisplayContent = temporaryDisplayContent; + recalculateGeneratedContentLines(); + highlightGeneratedContent(); + }); + return; + } + + // Generated content is intact - allow the change + temporaryDisplayContent = newContent; + + // Extract only the non-generated content for saving + const nonGeneratedLines = []; + const allLines = newContent.split("\n"); + + for (let i = 0; i < allLines.length; i++) { + // Only include lines that are NOT in the generated range + if (i < generatedContentStartLine || i > generatedContentEndLine) { + nonGeneratedLines.push(allLines[i]); + } + } - // Re-apply highlights immediately after any content change if we have generated content + const nonGeneratedContent = nonGeneratedLines.join("\n"); + onTestsChange({ ...tests, script: nonGeneratedContent }); + } else { + // Normal behavior when no generated content is protected + onTestsChange({ ...tests, script: newContent }); + } + + // Re-apply highlights if we have generated content if (showGeneratedTestActions) { if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { highlightGeneratedContent(); - // Double-check after a tiny delay - setTimeout(() => highlightGeneratedContent(), 1); }); } }; + // Helper function to recalculate line positions after content changes + const recalculateGeneratedContentLines = () => { + if (!generatedTestContent || !temporaryDisplayContent) return; + + const allLines = temporaryDisplayContent.split("\n"); + const generatedLines = generatedTestContent.split("\n"); + + // Only look for generated content in a very narrow range around the expected position + // This prevents matching similar content elsewhere + let foundStart = -1; + + // Search in a very small window around the current position + const searchStart = Math.max(0, generatedContentStartLine - 1); + const searchEnd = Math.min( + allLines.length - generatedLines.length + 1, + generatedContentStartLine + 3, // Very narrow search window + ); + + for (let i = searchStart; i < searchEnd; i++) { + let matches = true; + + // Check if ALL lines match exactly + for (let j = 0; j < generatedLines.length; j++) { + if (i + j >= allLines.length || allLines[i + j] !== generatedLines[j]) { + matches = false; + break; + } + } + + // Additional check: make sure we're not matching content that was there before + if (matches) { + // Verify this is actually the generated content block by checking context + // The generated content should be preceded by original content + separator + const hasProperContext = + i === 0 || // At the beginning + (originalTestContent && i > 0) || // After original content + (!originalTestContent && i === 0); // No original content, starts at beginning + + if (hasProperContext) { + foundStart = i; + break; + } + } + } + + if (foundStart >= 0) { + generatedContentStartLine = foundStart; + generatedContentEndLine = foundStart + generatedLines.length - 1; + } + }; + const toggleLeftPanel = (): void => { isLeftPanelCollapsed = !isLeftPanelCollapsed; }; + // Update the selectSnippet function to properly handle snippets without affecting generated content tracking const selectSnippet = (data: string): void => { - let value = tests?.script || ""; - value += value ? `\n${data}` : data; - onTestsChange({ ...tests, script: value }); + let value = showGeneratedTestActions + ? temporaryDisplayContent + : tests?.script || ""; + + const newValue = value ? `${value}\n${data}` : data; + + if (showGeneratedTestActions) { + const snippetLines = data.split("\n").length; + const addedNewlines = value ? 1 : 0; + + // Update the content first + temporaryDisplayContent = newValue; + + // Calculate where the snippet was inserted + const oldLines = value.split("\n"); + const insertionPoint = oldLines.length; + + // Only adjust if snippet was inserted before generated content + if (insertionPoint <= generatedContentStartLine) { + generatedContentStartLine += snippetLines + addedNewlines; + generatedContentEndLine += snippetLines + addedNewlines; + } + + // Track the snippet added during generation + contentAddedDuringGeneration = contentAddedDuringGeneration + ? `${contentAddedDuringGeneration}\n${data}` + : data; + + // DON'T update the stored tests.script during generation + // This preserves the original content for rejection + + // Reapply highlights + setTimeout(() => { + highlightGeneratedContent(); + }, 0); + } else { + // Normal behavior when no generated content is active + onTestsChange({ ...tests, script: newValue }); + } }; const highlightMatch = (text: string, searchTerm: string): string => { @@ -137,6 +279,7 @@ const handleGenerateTestCases = async () => { // Store the original content and current prompt originalTestContent = tests?.script || ""; + contentAddedDuringGeneration = ""; originalLineCount = originalTestContent.trim() ? originalTestContent.split("\n").length : 0; @@ -156,8 +299,8 @@ testCasePrompt = ""; generatedTestContent = result.generatedContent; - // Directly insert the generated content into the editor - await insertGeneratedContentDirectly(); + // Show generated content temporarily (don't save to tests object) + await showGeneratedContentTemporarily(); // Show the action buttons showGeneratedTestActions = true; @@ -167,28 +310,32 @@ testCasePrompt = ""; } }; - const insertGeneratedContentDirectly = async () => { - // Insert the generated test content into the current script - const currentScript = tests?.script || ""; - // Calculate the exact line positions where generated content will be placed - const currentLines = currentScript ? currentScript.split("\n") : []; + const showGeneratedContentTemporarily = async () => { + // Calculate line positions for highlighting + const currentScript = originalTestContent; const separator = currentScript ? "\n\n" : ""; - // Store where generated content starts - generatedContentStartLine = currentLines.length + (currentScript ? 2 : 0); + // Create the temporary display content + temporaryDisplayContent = currentScript + separator + generatedTestContent; - // Store where generated content ends - const generatedContentLines = generatedTestContent.split("\n"); - generatedContentEndLine = - generatedContentStartLine + generatedContentLines.length - 1; + // Calculate exact line positions based on the original content length + const originalLines = currentScript ? currentScript.split("\n") : []; + const separatorLines = separator ? separator.split("\n") : []; - const newScript = currentScript + separator + generatedTestContent; + // Calculate start position: original content + separator + generatedContentStartLine = + originalLines.length + separatorLines.length - 1; + if (currentScript && separator) { + generatedContentStartLine = originalLines.length + 1; // +1 for the empty line from separator + } else if (!currentScript) { + generatedContentStartLine = 0; + } - onTestsChange({ - ...tests, - script: newScript, - }); + // Calculate end position + const generatedLines = generatedTestContent.split("\n"); + generatedContentEndLine = + generatedContentStartLine + generatedLines.length - 1; await tick(); @@ -320,6 +467,7 @@ } }); }; + const removeHighlight = () => { if (observer) { observer.disconnect(); @@ -369,26 +517,47 @@ const acceptGeneratedTest = () => { removeHighlight(); - // Keep the generated content that's already in the editor + // Save the complete content (original + snippets + generated) + onTestsChange({ ...tests, script: temporaryDisplayContent }); + + // Clear temporary state showGeneratedTestActions = false; generatedTestContent = ""; originalTestContent = ""; + temporaryDisplayContent = ""; currentPrompt = ""; originalLineCount = 0; generatedContentStartLine = 0; generatedContentEndLine = 0; + contentAddedDuringGeneration = ""; // Clear snippets tracker }; const rejectGeneratedTest = () => { removeHighlight(); - // Revert to original content - onTestsChange({ ...tests, script: originalTestContent }); + // Calculate what content to revert to: + // Original content + any snippets added during generation + let revertedContent = originalTestContent || ""; + + if (contentAddedDuringGeneration) { + revertedContent = revertedContent + ? `${revertedContent}\n${contentAddedDuringGeneration}` + : contentAddedDuringGeneration; + } + + // Save the reverted content (original + snippets, but no AI generation) + onTestsChange({ ...tests, script: revertedContent }); + + // Clear all temporary state showGeneratedTestActions = false; generatedTestContent = ""; originalTestContent = ""; + temporaryDisplayContent = ""; currentPrompt = ""; originalLineCount = 0; + generatedContentStartLine = 0; + generatedContentEndLine = 0; + contentAddedDuringGeneration = ""; }; const regenerateTest = async () => { @@ -399,7 +568,7 @@ showGeneratedTestActions = false; // Revert to original content first - onTestsChange({ ...tests, script: originalTestContent }); + temporaryDisplayContent = originalTestContent; // Use the same prompt to regenerate const result = await onGenerateTestCases(currentPrompt); @@ -412,8 +581,8 @@ errorMessage = ""; generatedTestContent = result.generatedContent; - // Insert the new generated content - await insertGeneratedContentDirectly(); + // Show the new generated content temporarily + await showGeneratedContentTemporarily(); // Show the action buttons again showGeneratedTestActions = true; @@ -549,7 +718,9 @@ >