diff --git a/CHANGELOG.md b/CHANGELOG.md index e7080cf68..996729ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Snyk Security Changelog +## [2.12.0] +- Renders the AI Fix panel and adds more custom styling for VSCode. +- Adds position line interaction. -## [2.10.2] -- Remove snyk/codeclient dependancy. - -## [2.10.1] +## [2.11.0] - Add warning messages in the Tree View for the issue view options used in consistent ignores. - Add Data Flow and Ignore Footer intractions for Consistent Ignores flows. - Fix endpoint computation based on custom endpoint. +- Remove snyk/codeclient dependancy. ## [2.10.0] - Injects custom styling for the HTML panel used by Snyk Code for consistent ignores. diff --git a/media/views/snykCode/suggestion/suggestionLS.scss b/media/views/snykCode/suggestion/suggestionLS.scss index 6e155e491..b3b35bcb8 100644 --- a/media/views/snykCode/suggestion/suggestionLS.scss +++ b/media/views/snykCode/suggestion/suggestionLS.scss @@ -1,3 +1,23 @@ +@import '../../common/vscode'; +@import '../../common/webview'; + +body { + position: relative; + display: inline-flex; + flex-direction: column; + width: 100%; + height: 100%; + margin: 0; + font-size: 1.4rem; +} + +.severity-title { + font-size: 1.6rem; + font-weight: 500; + line-height: 2.4rem; + text-transform: capitalize; +} + .ignore-warning { background: #FFF4ED; color: #B6540B; @@ -14,8 +34,6 @@ color: var(--vscode-textLink-foreground); } -.tabs-nav {} - .tab-item { color: var(--vscode-foreground); border-bottom: 1px solid transparent; @@ -25,28 +43,38 @@ fill: var(--tab-item-github-icon-color); } -.tab-item:hover {} .tab-item.is-selected { border-bottom: 3px solid var(--vscode-focusBorder); } -.tab-content { - background-color: var(--vscode-editor-background); -} - .ignore-details-header, .data-flow-header, +.ai-fix-header, .example-fixes-header { text-transform: uppercase; } +.data-flow-number, +.data-flow-clickable-row, +.data-flow-delimiter, +.data-flow-text { + background-color: transparent; + border-color: transparent; + padding: 0px; +} + .ignore-details-tab, .fix-analysis-tab, .vuln-overview-tab { text-transform: uppercase; } +.example { + border: 1px solid var(--vscode-input-border); + background-color: var(--vscode-editor-background); +} + .example-line-number, .example-line>code { color: var(--vscode-editor-foreground); @@ -108,3 +136,7 @@ border-color: var(--vscode-button-hoverBackground); color: var(--vscode-button-foreground); } + +.sn-fix-wrapper { + background-color: var(--vscode-editor-background); +} diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScriptLS.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScriptLS.ts index b968b3585..3902a2fbc 100644 --- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScriptLS.ts +++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScriptLS.ts @@ -21,6 +21,10 @@ file: string; }; type Point = [number, number]; + type AutofixUnifiedDiffSuggestion = { + fixId: string; + unifiedDiffsPerFile: { [key: string]: string }; + }; type Suggestion = { id: string; message: string; @@ -32,8 +36,9 @@ markers?: Marker[]; cols: Point; rows: Point; - hasAIFix: boolean; filePath: string; + hasAIFix: boolean; + diffs: AutofixUnifiedDiffSuggestion[]; }; type OpenLocalMessage = { @@ -69,7 +74,45 @@ type: 'get'; }; - type SuggestionMessage = OpenLocalMessage | IgnoreIssueMessage | SetSuggestionMessage | GetSuggestionMessage; + type GetAutofixDiffsMesssage = { + type: 'getAutofixDiffs'; + args: { + suggestion: Suggestion; + }; + }; + + type ApplyGitDiffMessage = { + type: 'applyGitDiff'; + args: { + patch: string; + filePath: string; + }; + }; + + type SetAutofixDiffsMessage = { + type: 'setAutofixDiffs'; + args: { + suggestion: Suggestion; + diffs: AutofixUnifiedDiffSuggestion[]; + }; + }; + + type SetAutofixErrorMessage = { + type: 'setAutofixError'; + args: { + suggestion: Suggestion; + }; + }; + + type SuggestionMessage = + | OpenLocalMessage + | IgnoreIssueMessage + | SetSuggestionMessage + | GetSuggestionMessage + | GetAutofixDiffsMesssage + | ApplyGitDiffMessage + | SetAutofixDiffsMessage + | SetAutofixErrorMessage; const vscode = acquireVsCodeApi(); @@ -77,8 +120,20 @@ vscode.postMessage(message); } - function navigateToIssue(_e: any, position: MarkerPosition) { - if (!suggestion) return; + function navigateToIssue(i: number) { + if (!suggestion) { + return; + } + const markers = suggestion.markers; + if (!markers) { + return; + } + const position = { + file: suggestion.filePath, + rows: markers[i].pos[0].rows, + cols: markers[i].pos[0].cols, + }; + const message: OpenLocalMessage = { type: 'openLocal', args: { @@ -119,27 +174,178 @@ const dataFlows = document.getElementsByClassName('data-flow-clickable-row'); for (let i = 0; i < dataFlows.length; i++) { - dataFlows[i].addEventListener('click', e => { - if (!suggestion) { - return; - } - const markers = suggestion.markers; - if (!markers) { - return; - } - navigateToIssue(e, { - file: suggestion.filePath, - rows: markers[i].pos[0].rows, - cols: markers[i].pos[0].cols, - }); + dataFlows[i].addEventListener('click', () => { + navigateToIssue(i); }); } - document.getElementById('ignore-line-issue')!.addEventListener('click', () => { + document.getElementById('ignore-line-issue')?.addEventListener('click', () => { ignoreIssue(true); }); - document.getElementById('ignore-file-issue')!.addEventListener('click', () => { + document.getElementById('ignore-file-issue')?.addEventListener('click', () => { ignoreIssue(false); }); + document.getElementById('position-line')!.addEventListener('click', () => { + navigateToIssue(0); + }); + + function toggleElement(element: Element | null, toggle: 'hide' | 'show') { + if (!element) { + return; + } + + if (toggle === 'show') { + element.classList.remove('hidden'); + } else if (toggle === 'hide') { + element.classList.add('hidden'); + } else { + console.error('Unexpected toggle value', toggle); + } + } + + // different AI fix buttons + const applyFixButton = document.getElementById('apply-fix') as HTMLElement; + const retryGenerateFixButton = document.getElementById('retry-generate-fix') as HTMLElement; + const generateAIFixButton = document.getElementById('generate-ai-fix') as HTMLElement; + + function generateAIFix() { + if (!suggestion) { + return; + } + + toggleElement(generateAIFixButton, 'hide'); + toggleElement(fixLoadingIndicatorElem, 'show'); + const message: GetAutofixDiffsMesssage = { + type: 'getAutofixDiffs', + args: { suggestion }, + }; + sendMessage(message); + } + + function retryGenerateAIFix() { + console.log('retrying generate AI Fix'); + + toggleElement(fixWrapperElem, 'show'); + toggleElement(fixErrorSectionElem, 'hide'); + + generateAIFix(); + } + + function applyFix() { + if (!suggestion) return; + const diffSuggestion = suggestion.diffs[diffSelectedIndex]; + const filePath = suggestion.filePath; + const patch = diffSuggestion.unifiedDiffsPerFile[filePath]; + + const message: ApplyGitDiffMessage = { + type: 'applyGitDiff', + args: { filePath, patch }, + }; + sendMessage(message); + } + + generateAIFixButton?.addEventListener('click', generateAIFix); + retryGenerateFixButton?.addEventListener('click', retryGenerateAIFix); + applyFixButton?.addEventListener('click', applyFix); + + // different AI fix states + const fixLoadingIndicatorElem = document.getElementById('fix-loading-indicator') as HTMLElement; + const fixWrapperElem = document.getElementById('fix-wrapper') as HTMLElement; + const fixSectionElem = document.getElementById('fixes-section') as HTMLElement; + const fixErrorSectionElem = document.getElementById('fixes-error-section') as HTMLElement; + + // generated AI fix diffs + const nextDiffElem = document.getElementById('next-diff') as HTMLElement; + const previousDiffElem = document.getElementById('previous-diff') as HTMLElement; + const diffSelectedIndexElem = document.getElementById('diff-counter') as HTMLElement; + + const diffTopElem = document.getElementById('diff-top') as HTMLElement; + const diffElem = document.getElementById('diff') as HTMLElement; + const noDiffsElem = document.getElementById('info-no-diffs') as HTMLElement; + + const diffNumElem = document.getElementById('diff-number') as HTMLElement; + const diffNum2Elem = document.getElementById('diff-number2') as HTMLElement; + + let diffSelectedIndex = 0; + + function nextDiff() { + if (!suggestion || !suggestion.diffs || diffSelectedIndex >= suggestion.diffs.length - 1) return; + ++diffSelectedIndex; + showCurrentDiff(); + } + + function previousDiff() { + if (!suggestion || !suggestion.diffs || diffSelectedIndex <= 0) return; + --diffSelectedIndex; + showCurrentDiff(); + } + + function showCurrentDiff() { + if (!suggestion?.diffs?.length || diffSelectedIndex < 0 || diffSelectedIndex >= suggestion.diffs.length) return; + + toggleElement(noDiffsElem, 'hide'); + toggleElement(diffTopElem, 'show'); + toggleElement(diffElem, 'show'); + + diffNumElem.innerText = suggestion.diffs.length.toString(); + diffNum2Elem.innerText = suggestion.diffs.length.toString(); + + diffSelectedIndexElem.innerText = (diffSelectedIndex + 1).toString(); + + const diffSuggestion = suggestion.diffs[diffSelectedIndex]; + + const filePath = suggestion.filePath; + const patch = diffSuggestion.unifiedDiffsPerFile[filePath]; + + // clear all elements + while (diffElem.firstChild) { + diffElem.removeChild(diffElem.firstChild); + } + diffElem.appendChild(generateDiffHtml(patch)); + } + + function generateDiffHtml(patch: string): HTMLElement { + const codeLines = patch.split('\n'); + + // the first two lines are the file names + codeLines.shift(); + codeLines.shift(); + + const diffHtml = document.createElement('div'); + let blockDiv: HTMLElement | null = null; + + for (const line of codeLines) { + if (line.startsWith('@@ ')) { + blockDiv = document.createElement('div'); + blockDiv.className = 'example'; + + if (blockDiv) { + diffHtml.appendChild(blockDiv); + } + } else { + const lineDiv = document.createElement('div'); + lineDiv.className = 'example-line'; + + if (line.startsWith('+')) { + lineDiv.classList.add('added'); + } else if (line.startsWith('-')) { + lineDiv.classList.add('removed'); + } + + const lineCode = document.createElement('code'); + // if line is empty, we need to fallback to ' ' + // to make sure it displays in the diff + lineCode.innerText = line.slice(1, line.length) || ' '; + + lineDiv.appendChild(lineCode); + blockDiv?.appendChild(lineDiv); + } + } + + return diffHtml; + } + + nextDiffElem.addEventListener('click', nextDiff); + previousDiffElem.addEventListener('click', previousDiff); window.addEventListener('message', event => { const message = event.data as SuggestionMessage; @@ -156,6 +362,30 @@ } break; } + case 'setAutofixDiffs': { + if (suggestion?.id === message.args.suggestion.id) { + toggleElement(fixSectionElem, 'show'); + toggleElement(fixLoadingIndicatorElem, 'hide'); + toggleElement(fixWrapperElem, 'hide'); + + const { diffs } = message.args; + suggestion.diffs = diffs; + + vscode.setState({ ...vscode.getState(), suggestion }); + showCurrentDiff(); + } + break; + } + case 'setAutofixError': { + const errorSuggestion = message.args.suggestion; + + if (errorSuggestion.id != suggestion?.id) { + console.log('Got an error for a previously generated suggestion: ignoring'); + break; + } + toggleElement(fixWrapperElem, 'hide'); + toggleElement(fixErrorSectionElem, 'show'); + } } }); })();