From 44d1a82e6f1e587c6ff14391cd42239d59425d85 Mon Sep 17 00:00:00 2001 From: Tan Nhu Date: Mon, 13 May 2024 18:35:07 +0000 Subject: [PATCH] Pull Request Code suggestions (#2032) --- web/package.json | 1 + web/src/atoms/pullReqSuggestions.ts | 24 ++ web/src/components/Changes/Changes.tsx | 44 ++- .../CommentBox/CommentBox.module.scss | 4 + web/src/components/CommentBox/CommentBox.tsx | 97 +++--- .../CommentThreadTopDecoration.tsx | 38 +++ .../useCommitSuggestionModal.tsx | 150 +++++++++ .../components/DiffViewer/DiffViewerUtils.tsx | 38 ++- .../DiffViewer/usePullReqComments.tsx | 36 +-- .../MarkdownEditorWithPreview.tsx | 303 ++++++++++-------- .../MarkdownViewer/CodeSuggestionBlock.tsx | 227 +++++++++++++ .../MarkdownViewer/MarkdownViewer.module.scss | 29 ++ .../MarkdownViewer.module.scss.d.ts | 4 + .../MarkdownViewer/MarkdownViewer.tsx | 38 ++- .../PullReqSuggestionsBatch.module.scss | 11 + .../PullReqSuggestionsBatch.module.scss.d.ts | 19 ++ .../PullReqSuggestionsBatch.tsx | 98 ++++++ .../SuggestionBlock/SuggestionBlock.tsx | 19 ++ web/src/framework/strings/stringTypes.ts | 15 + web/src/i18n/strings.en.yaml | 19 +- web/src/pages/PullRequest/Checks/Checks.tsx | 4 +- .../Conversation/CodeCommentHeader.tsx | 4 +- .../PullRequest/Conversation/Conversation.tsx | 44 ++- .../PullRequestActionsBox.tsx | 2 + web/src/pages/PullRequest/PullRequest.tsx | 2 +- .../PullRequestCommits/PullRequestCommits.tsx | 3 +- .../PullRequestTabContentWrapper.tsx | 6 +- .../pages/PullRequest/PullRequestUtils.tsx | 10 + .../PullRequest/useGetPullRequestInfo.ts | 4 +- web/tsconfig.json | 2 +- web/yarn.lock | 257 ++++++++++++++- 31 files changed, 1318 insertions(+), 234 deletions(-) create mode 100644 web/src/atoms/pullReqSuggestions.ts create mode 100644 web/src/components/CommentThreadTopDecoration/CommentThreadTopDecoration.tsx create mode 100644 web/src/components/CommitModalButton/useCommitSuggestionModal.tsx create mode 100644 web/src/components/MarkdownViewer/CodeSuggestionBlock.tsx create mode 100644 web/src/components/PullReqSuggestionsBatch/PullReqSuggestionsBatch.module.scss create mode 100644 web/src/components/PullReqSuggestionsBatch/PullReqSuggestionsBatch.module.scss.d.ts create mode 100644 web/src/components/PullReqSuggestionsBatch/PullReqSuggestionsBatch.tsx create mode 100644 web/src/components/SuggestionBlock/SuggestionBlock.tsx diff --git a/web/package.json b/web/package.json index b340c62d3..3ecc6665f 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "diff2html": "3.4.22", "dompurify": "^3.0.5", "formik": "2.2.9", + "hast-util-to-html": "^9.0.1", "highlight.js": "^11.8.0", "iconoir-react": "^6.11.0", "jotai": "^2.6.3", diff --git a/web/src/atoms/pullReqSuggestions.ts b/web/src/atoms/pullReqSuggestions.ts new file mode 100644 index 000000000..25f26e018 --- /dev/null +++ b/web/src/atoms/pullReqSuggestions.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { atom } from 'jotai' + +export interface Suggestion { + check_sum: string + comment_id: number +} + +export const pullReqSuggestionsAtom = atom([]) diff --git a/web/src/components/Changes/Changes.tsx b/web/src/components/Changes/Changes.tsx index 88422e25a..f7bf39941 100644 --- a/web/src/components/Changes/Changes.tsx +++ b/web/src/components/Changes/Changes.tsx @@ -54,10 +54,12 @@ import { useAppContext } from 'AppContext' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { createRequestIdleCallbackTaskPool } from 'utils/Task' import { PlainButton } from 'components/PlainButton/PlainButton' -import { dispatchCustomEvent, useEventListener } from 'hooks/useEventListener' +import { dispatchCustomEvent, useCustomEventListener, useEventListener } from 'hooks/useEventListener' import type { UseGetPullRequestInfoResult } from 'pages/PullRequest/useGetPullRequestInfo' import { InViewDiffBlockRenderer } from 'components/DiffViewer/InViewDiffBlockRenderer' import Config from 'Config' +import { PullReqSuggestionsBatch } from 'components/PullReqSuggestionsBatch/PullReqSuggestionsBatch' +import { PullReqCustomEvent } from 'pages/PullRequest/PullRequestUtils' import { ChangesDropdown } from './ChangesDropdown' import { DiffViewConfiguration } from './DiffViewConfiguration' import ReviewSplitButton from './ReviewSplitButton/ReviewSplitButton' @@ -406,6 +408,16 @@ const ChangesInternal: React.FC = ({ [diffBlocks, isMounted] ) + const refreshPullReq = useCallback(() => { + setCachedDiff({}) + setTargetRef(_targetRef) + setSourceRef(_sourceRef) + setPrHasChanged(false) + refetchCommits?.() + }, [_sourceRef, _targetRef, refetchCommits, setCachedDiff]) + + useCustomEventListener(PullReqCustomEvent.REFETCH_DIFF, refreshPullReq) + /* * Jump to file and comment if they are specified in URL. When path and commentId * are specified in URL, leverage onJumpToFile() to jump to file, then comment. @@ -430,6 +442,7 @@ const ChangesInternal: React.FC = ({ }, [diffs, setPullReqChangesCount]) useShowRequestError(errorFileViews, 0) + return ( @@ -471,13 +484,8 @@ const ChangesInternal: React.FC = ({ { - setCachedDiff({}) - setTargetRef(_targetRef) - setSourceRef(_sourceRef) - setPrHasChanged(false) - refetchCommits?.() - }} + onClick={refreshPullReq} + data-button-name="refresh-pr" /> @@ -497,13 +505,19 @@ const ChangesInternal: React.FC = ({ - + + + + + + + diff --git a/web/src/components/CommentBox/CommentBox.module.scss b/web/src/components/CommentBox/CommentBox.module.scss index 30a057cc1..cc12a4eba 100644 --- a/web/src/components/CommentBox/CommentBox.module.scss +++ b/web/src/components/CommentBox/CommentBox.module.scss @@ -38,6 +38,10 @@ } } + &:has([data-outdated='true']) [data-section-id='CodeSuggestionBlockButtons'] { + display: none; + } + .box { box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16), 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16); diff --git a/web/src/components/CommentBox/CommentBox.tsx b/web/src/components/CommentBox/CommentBox.tsx index b00af6220..44767486a 100644 --- a/web/src/components/CommentBox/CommentBox.tsx +++ b/web/src/components/CommentBox/CommentBox.tsx @@ -14,26 +14,13 @@ * limitations under the License. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import type { EditorView } from '@codemirror/view' import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match' -import { - Container, - Layout, - Avatar, - TextInput, - Text, - FlexExpander, - Button, - useIsMounted, - ButtonVariation, - ButtonSize, - StringSubstitute, - AvatarGroup -} from '@harnessio/uicore' +import { Container, Layout, Avatar, TextInput, Text, FlexExpander, Button, useIsMounted } from '@harnessio/uicore' import { Color, FontVariation } from '@harnessio/design-system' import cx from 'classnames' -import { isEqual, noop, defaultTo, get, uniq } from 'lodash-es' +import { isEqual, noop, defaultTo, get } from 'lodash-es' import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal' import { useStrings } from 'framework/strings' import { ThreadSection } from 'components/ThreadSection/ThreadSection' @@ -46,6 +33,7 @@ import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer' import { ButtonRoleProps } from 'utils/Utils' import { useResizeObserver } from 'hooks/useResizeObserver' import { useCustomEventListener } from 'hooks/useEventListener' +import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock' import css from './CommentBox.module.scss' export interface CommentItem { @@ -108,6 +96,7 @@ interface CommentBoxProps { standalone: boolean routingId: string copyLinkToComment: (commentId: number, commentItem: CommentItem) => void + suggestionBlock?: SuggestionBlock } const CommentBoxInternal = ({ @@ -131,7 +120,8 @@ const CommentBoxInternal = ({ repoMetadata, standalone, routingId, - copyLinkToComment + copyLinkToComment, + suggestionBlock }: CommentBoxProps) => { const { getString } = useStrings() const [comments, setComments] = useState[]>(commentItems) @@ -217,6 +207,7 @@ const CommentBoxInternal = ({ }} outlets={outlets} copyLinkToComment={copyLinkToComment} + suggestionBlock={suggestionBlock} /> @@ -298,6 +289,7 @@ const CommentBoxInternal = ({ setDirties({ ...dirties, ['new']: _dirty }) }} autoFocusAndPosition={autoFocusAndPosition ? !showReplyPlaceHolder : false} + suggestionBlock={suggestionBlock} /> @@ -310,7 +302,10 @@ const CommentBoxInternal = ({ } interface CommentsThreadProps - extends Pick, 'commentItems' | 'handleAction' | 'outlets' | 'copyLinkToComment'> { + extends Pick< + CommentBoxProps, + 'commentItems' | 'handleAction' | 'outlets' | 'copyLinkToComment' | 'suggestionBlock' + > { onQuote: (content: string) => void setDirty: (index: number, dirty: boolean) => void repoMetadata: TypesRepository | undefined @@ -323,7 +318,8 @@ const CommentsThread = ({ setDirty, outlets = {}, repoMetadata, - copyLinkToComment + copyLinkToComment, + suggestionBlock }: CommentsThreadProps) => { const { getString } = useStrings() const { standalone, routingId } = useAppContext() @@ -335,22 +331,22 @@ const CommentsThread = ({ }, [editIndexes] ) - const collapseResolvedComments = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems]) - const shouldCollapsedResolvedComments = useMemo( - () => - collapseResolvedComments && - !(commentItems.length === 1 && shorten(commentItems[0].content) === commentItems[0].content), - [commentItems, collapseResolvedComments] - ) - const [collapsed, setCollapsed] = useState(collapseResolvedComments) + // const collapseResolvedComments = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems]) + // const shouldCollapsedResolvedComments = useMemo( + // () => + // collapseResolvedComments && + // !(commentItems.length === 1 && shorten(commentItems[0].content) === commentItems[0].content), + // [commentItems, collapseResolvedComments] + // ) + // const [collapsed, setCollapsed] = useState(collapseResolvedComments) return ( {commentItems - .filter((_commentItem, index) => { - return collapseResolvedComments && collapsed ? index === 0 : true - }) + // .filter((_commentItem, index) => { + // return collapseResolvedComments && collapsed ? index === 0 : true + // }) .map((commentItem, index) => { const isLastItem = index === commentItems.length - 1 @@ -358,7 +354,10 @@ const CommentsThread = ({ + @@ -444,7 +443,7 @@ const CommentsThread = ({ } - hideGutter={isLastItem || (collapseResolvedComments && collapsed)}> + hideGutter={isLastItem /*|| (collapseResolvedComments && collapsed)*/}> @@ -478,6 +477,7 @@ const CommentsThread = ({ cancel: getString('cancel') }} autoFocusAndPosition + suggestionBlock={suggestionBlock} /> @@ -489,10 +489,23 @@ const CommentsThread = ({ @@ -503,7 +516,7 @@ const CommentsThread = ({ ) })} - + {/* ({ - + */} ) @@ -548,11 +561,11 @@ export const CommentBox = React.memo(CommentBoxInternal) export const customEventForCommentWithId = (id: number) => `CommentBoxCustomEvent-${id}` -const shorten = (str = '', maxLen = 140, separator = ' ') => { - const s = str.split('\n')[0] - const sub = s.length <= maxLen ? s : s.substr(0, s.lastIndexOf(separator, maxLen)) +// const shorten = (str = '', maxLen = 140, separator = ' ') => { +// const s = str.split('\n')[0] +// const sub = s.length <= maxLen ? s : s.substr(0, s.lastIndexOf(separator, maxLen)) - return sub.length < str.length ? sub + '...' : sub -} +// return sub.length < str.length ? sub + '...' : sub +// } const CRLF = '\n' diff --git a/web/src/components/CommentThreadTopDecoration/CommentThreadTopDecoration.tsx b/web/src/components/CommentThreadTopDecoration/CommentThreadTopDecoration.tsx new file mode 100644 index 000000000..b5489877f --- /dev/null +++ b/web/src/components/CommentThreadTopDecoration/CommentThreadTopDecoration.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react' +import { Text } from '@harnessio/uicore' +import { Color, FontVariation } from '@harnessio/design-system' +import { useStrings } from 'framework/strings' + +export const CommentThreadTopDecoration: React.FC<{ startLine: number; endLine: number }> = ({ + startLine, + endLine +}) => { + const { getString } = useStrings() + + return startLine !== endLine ? ( + + {getString('pr.commentLineNumbers', { start: startLine, end: endLine })} + + ) : null +} diff --git a/web/src/components/CommitModalButton/useCommitSuggestionModal.tsx b/web/src/components/CommitModalButton/useCommitSuggestionModal.tsx new file mode 100644 index 000000000..c224eac9d --- /dev/null +++ b/web/src/components/CommitModalButton/useCommitSuggestionModal.tsx @@ -0,0 +1,150 @@ +/* + * Copyright 2023 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState } from 'react' +import { Dialog, Intent } from '@blueprintjs/core' +import { + Button, + Container, + Layout, + FlexExpander, + Formik, + FormikForm, + Heading, + useToaster, + FormInput, + ButtonVariation +} from '@harnessio/uicore' +import { useAtomValue } from 'jotai' +import { Icon } from '@harnessio/icons' +import { useMutate } from 'restful-react' +import cx from 'classnames' +import { FontVariation } from '@harnessio/design-system' +import { useModalHook } from 'hooks/useModalHook' +import { useStrings } from 'framework/strings' +import { pullReqAtom } from 'pages/PullRequest/useGetPullRequestInfo' +import { repoMetadataAtom } from 'atoms/repoMetadata' +import css from './CommitModalButton.module.scss' + +interface FormData { + commitMessage?: string + extendedDescription?: string +} + +interface CommitModalProps extends FormData { + title?: string + onCommit: (formData: FormData) => Promise> +} + +export function useCommitSuggestionsModal({ + title = '', + commitMessage = '', + extendedDescription = '', + onCommit +}: CommitModalProps) { + const ModalComponent: React.FC = () => { + const { getString } = useStrings() + const { showError } = useToaster() + const [loading, setLoading] = useState(false) + const onSubmit = useCallback( + async (formData: FormData) => { + setLoading(true) + const error = await onCommit({ + commitMessage: formData.commitMessage || '', + extendedDescription: formData.extendedDescription || '' + }) + setLoading(false) + + if (error) { + showError(error) + } else { + hideModal() + } + }, + [showError] + ) + + return ( + + + + {title || getString('commitChanges')} + + + + + initialValues={{ + commitMessage, + extendedDescription + }} + formName="commitChanges" + enableReinitialize={true} + validateOnChange + validateOnBlur + onSubmit={onSubmit}> + + + + + + + ) + } + + const [openModal, hideModal] = useModalHook(ModalComponent, []) + + return [openModal, hideModal] +} + +export function useCommitPullReqSuggestions() { + const repoMetadata = useAtomValue(repoMetadataAtom) + const pullReq = useAtomValue(pullReqAtom) + const { mutate } = useMutate({ + verb: 'POST', + path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullReq?.number}/comments/apply-suggestions` + }) + + return mutate +} diff --git a/web/src/components/DiffViewer/DiffViewerUtils.tsx b/web/src/components/DiffViewer/DiffViewerUtils.tsx index ccdec3645..502c156ef 100644 --- a/web/src/components/DiffViewer/DiffViewerUtils.tsx +++ b/web/src/components/DiffViewer/DiffViewerUtils.tsx @@ -67,6 +67,7 @@ export interface DiffCommentItem { commentItems: CommentItem[] _commentItems?: CommentItem[] filePath: string + codeBlockContent?: string destroy: (() => void) | undefined } @@ -245,6 +246,40 @@ export function activitiesToDiffCommentItems( const lineNumberStart = (right ? activity.code_comment?.line_new : activity.code_comment?.line_old) as number const lineNumberEnd = lineNumberStart + span - 1 + const diffSnapshotLines = get(activity.payload, 'lines', []) as string[] + const leftLines: string[] = [] + const rightLines: string[] = [] + + diffSnapshotLines.forEach(line => { + const lineContent = line.substring(1) // line has a `prefix` (space, +, or -), always remove it + + if (line.startsWith('-')) { + leftLines.push(lineContent) + } else if (line.startsWith('+')) { + rightLines.push(lineContent) + } else { + leftLines.push(lineContent) + rightLines.push(lineContent) + } + }) + const diffHeader = get(activity.payload, 'title', '') as string + const [oldStartLine, newStartLine] = diffHeader + .replaceAll(/@|\+|-/g, '') + .trim() + .split(' ') + .map(token => token.split(',')[0]) + .map(Number) + const _startLine = right ? newStartLine : oldStartLine + const codeLines = right ? rightLines : leftLines + let lineIndex = 0 + + while (lineIndex + _startLine < lineNumberStart) { + lineIndex++ + } + const codeBlockContent = codeLines + .slice(lineNumberStart - _startLine, lineNumberStart - _startLine + lineNumberEnd - lineNumberStart + 1) + .join('\n') + return { inner: activity, left: !right, @@ -256,7 +291,8 @@ export function activitiesToDiffCommentItems( commentItems: [activityToCommentItem(activity)].concat(replyComments), filePath: filePath, destroy: undefined, - eventStream: undefined + eventStream: undefined, + codeBlockContent } }) || [] ) diff --git a/web/src/components/DiffViewer/usePullReqComments.tsx b/web/src/components/DiffViewer/usePullReqComments.tsx index be8c04128..dda72dcd2 100644 --- a/web/src/components/DiffViewer/usePullReqComments.tsx +++ b/web/src/components/DiffViewer/usePullReqComments.tsx @@ -19,8 +19,7 @@ import { useMutate } from 'restful-react' import Selecto from 'selecto' import ReactDOM from 'react-dom' import { useLocation } from 'react-router-dom' -import { useToaster, ButtonProps, Utils, Text } from '@harnessio/uicore' -import { Color, FontVariation } from '@harnessio/design-system' +import { useToaster, ButtonProps, Utils } from '@harnessio/uicore' import { findLastIndex, isEqual, max, noop, random, uniq } from 'lodash-es' import { useStrings } from 'framework/strings' import type { GitInfoProps } from 'utils/GitUtils' @@ -28,13 +27,15 @@ import type { DiffFileEntry } from 'utils/types' import { useConfirmAct } from 'hooks/useConfirmAction' import { useAppContext } from 'AppContext' import type { OpenapiCommentCreatePullReqRequest, TypesPullReq, TypesPullReqActivity } from 'services/code' -import { PullRequestSection, getErrorMessage } from 'utils/Utils' +import { PullRequestSection, filenameToLanguage, getErrorMessage } from 'utils/Utils' import { AppWrapper } from 'App' import { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/CodeCommentStatusButton' import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondarySaveButton/CodeCommentSecondarySaveButton' import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect' import { dispatchCustomEvent } from 'hooks/useEventListener' import { UseGetPullRequestInfoResult, usePullReqActivities } from 'pages/PullRequest/useGetPullRequestInfo' +import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration' +import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock' import { activitiesToDiffCommentItems, activityToCommentItem, @@ -273,6 +274,15 @@ export function usePullReqComments({ // update to the latest data comment._commentItems = structuredClone(comment.commentItems) + const suggestionBlock: SuggestionBlock = { + source: + comment.codeBlockContent || + (lineElements?.length + ? lineElements.map(td => td.nextElementSibling?.querySelector('.d2h-code-line-ctn')?.textContent).join('\n') + : lineInfo.rowElement?.lastElementChild?.querySelector('.d2h-code-line-ctn')?.textContent || ''), + lang: filenameToLanguage(diff.filePath.split('/').pop()) + } + // Note: CommentBox is rendered as an independent React component. // Everything passed to it must be either values, or refs. // If you pass callbacks or states, they won't be updated and @@ -302,6 +312,7 @@ export function usePullReqComments({ setDirty={setDirty || noop} currentUserName={currentUser?.display_name || currentUser?.email || ''} copyLinkToComment={copyLinkToComment} + suggestionBlock={suggestionBlock} handleAction={async (action, value, commentItem) => { let result = true let updatedItem: CommentItem | undefined = undefined @@ -415,7 +426,9 @@ export function usePullReqComments({ return [result, updatedItem] }} outlets={{ - [CommentBoxOutletPosition.TOP]: , + [CommentBoxOutletPosition.TOP]: ( + + ), [CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]: ( ) { return !!ref.current?.querySelector('[data]' || !!ref.current?.querySelector('.d2h-wrapper')) } -const CommentThreadTopDecoration: React.FC<{ comment: DiffCommentItem }> = ({ comment }) => { - const { getString } = useStrings() - const { lineNumberStart: start, lineNumberEnd: end } = comment - - return start !== end ? ( - - {getString('pr.commentLineNumbers', { start, end })} - - ) : null -} - const selected = 'selected' diff --git a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx index 720c706be..2776e1711 100644 --- a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx +++ b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Text, Button, @@ -51,6 +51,7 @@ import { defaultUsefulOrNot } from 'components/DefaultUsefulOrNot/UsefulOrNot' import { AidaClient } from 'utils/types' import type { TypesRepository } from 'services/code' import { useEventListener } from 'hooks/useEventListener' +import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock' import css from './MarkdownEditorWithPreview.module.scss' enum MarkdownEditorTab { @@ -65,25 +66,17 @@ enum ToolbarAction { UPLOAD = 'UPLOAD', UNORDER_LIST = 'UNORDER_LIST', CHECK_LIST = 'CHECK_LIST', - CODE_BLOCK = 'CODE_BLOCK' + CODE_BLOCK = 'CODE_BLOCK', + SUGGESTION = 'SUGGESTION' } interface ToolbarItem { icon: IconName action: ToolbarAction + title?: string + size?: number } -const toolbar: ToolbarItem[] = [ - { icon: 'header', action: ToolbarAction.HEADER }, - { icon: 'bold', action: ToolbarAction.BOLD }, - { icon: 'italic', action: ToolbarAction.ITALIC }, - { icon: 'paperclip', action: ToolbarAction.UPLOAD }, - - { icon: 'properties', action: ToolbarAction.UNORDER_LIST }, - { icon: 'form', action: ToolbarAction.CHECK_LIST }, - { icon: 'main-code-yaml', action: ToolbarAction.CODE_BLOCK } -] - // Define a unique effect to update decorations const addDecorationEffect = StateEffect.define<{ decoration: Decoration; from: number; to: number }[]>() const removeDecorationEffect = StateEffect.define<{}>() // No payload needed for removal in this simple case// Create a state field to hold decorations @@ -144,6 +137,7 @@ interface MarkdownEditorWithPreviewProps { repoMetadata: TypesRepository | undefined standalone: boolean routingId: string + suggestionBlock?: SuggestionBlock } export function MarkdownEditorWithPreview({ @@ -170,7 +164,8 @@ export function MarkdownEditorWithPreview({ setFlag, flag, sourceGitRef, - targetGitRef + targetGitRef, + suggestionBlock }: MarkdownEditorWithPreviewProps) { const { getString } = useStrings() const fileInputRef = useRef(null) @@ -192,6 +187,22 @@ export function MarkdownEditorWithPreview({ }) const isDirty = useRef(dirty) const [data, setData] = useState({}) + const toolbar: ToolbarItem[] = useMemo(() => { + const initial: ToolbarItem[] = suggestionBlock + ? [{ icon: 'suggestion', action: ToolbarAction.SUGGESTION, title: getString('suggestion'), size: 20 }] + : [] + + return [ + ...initial, + { icon: 'header', action: ToolbarAction.HEADER, title: getString('heading') }, + { icon: 'bold', action: ToolbarAction.BOLD, title: getString('bold') }, + { icon: 'italic', action: ToolbarAction.ITALIC, title: getString('italic') }, + { icon: 'paperclip', action: ToolbarAction.UPLOAD, title: getString('upload') }, + { icon: 'properties', action: ToolbarAction.UNORDER_LIST, title: getString('unorderedList') }, + { icon: 'form', action: ToolbarAction.CHECK_LIST, title: getString('checklist') }, + { icon: 'main-code-yaml', action: ToolbarAction.CODE_BLOCK, title: getString('code') } + ] + }, [getString, suggestionBlock]) useEffect( function setDirtyRef() { @@ -200,7 +211,7 @@ export function MarkdownEditorWithPreview({ [dirty] ) - const myKeymap = keymap.of([ + const shortcuts = keymap.of([ { key: 'Mod-z', run: undo, @@ -214,8 +225,19 @@ export function MarkdownEditorWithPreview({ return true }, preventDefault: true + }, + { + key: 'Ctrl-g', + run: () => { + if (suggestionBlock) { + onToolbarAction(ToolbarAction.SUGGESTION) + } + return true + }, + preventDefault: !!suggestionBlock } ]) + const handleMouseDown = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any (event: any) => { @@ -289,128 +311,146 @@ export function MarkdownEditorWithPreview({ } }, [data]) // eslint-disable-line react-hooks/exhaustive-deps - const onToolbarAction = useCallback((action: ToolbarAction) => { - const view = viewRef.current + const onToolbarAction = useCallback( + (action: ToolbarAction) => { + const view = viewRef.current - if (!view?.state) { - return - } + if (!view?.state) { + return + } - // Note: Part of this code is copied from @uiwjs/react-markdown-editor - // MIT License, Copyright (c) 2020 uiw - // @see https://github.dev/uiwjs/react-markdown-editor/blob/2d3f45079c79616b867ef03681a8ba9799169921/src/commands/header.tsx - switch (action) { - case ToolbarAction.HEADER: { - const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) - let mark = '#' - const matchMark = lineInfo.text.match(/^#+/) - if (matchMark && matchMark[0]) { - const txt = matchMark[0] - if (txt.length < 6) { - mark = txt + '#' + // Note: Part of this code is copied from @uiwjs/react-markdown-editor + // MIT License, Copyright (c) 2020 uiw + // @see https://github.dev/uiwjs/react-markdown-editor/blob/2d3f45079c79616b867ef03681a8ba9799169921/src/commands/header.tsx + switch (action) { + case ToolbarAction.HEADER: { + const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) + let mark = '#' + const matchMark = lineInfo.text.match(/^#+/) + if (matchMark && matchMark[0]) { + const txt = matchMark[0] + if (txt.length < 6) { + mark = txt + '#' + } } + if (mark.length > 6) { + mark = '#' + } + const title = lineInfo.text.replace(/^#+/, '') + view.dispatch({ + changes: { + from: lineInfo.from, + to: lineInfo.to, + insert: `${mark} ${title}` + }, + // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), + selection: { anchor: lineInfo.from + mark.length + 1 } + }) + break } - if (mark.length > 6) { - mark = '#' + + case ToolbarAction.UPLOAD: { + setFile(undefined) + setOpen(true) + break } - const title = lineInfo.text.replace(/^#+/, '') - view.dispatch({ - changes: { - from: lineInfo.from, - to: lineInfo.to, - insert: `${mark} ${title}` - }, - // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), - selection: { anchor: lineInfo.from + mark.length + 1 } - }) - break - } - case ToolbarAction.UPLOAD: { - setFile(undefined) - setOpen(true) - break - } - - case ToolbarAction.BOLD: { - view.dispatch( - view.state.changeByRange(range => ({ - changes: [ - { from: range.from, insert: '**' }, - { from: range.to, insert: '**' } - ], - range: EditorSelection.range(range.from + 2, range.to + 2) - })) - ) - break - } - - case ToolbarAction.ITALIC: { - view.dispatch( - view.state.changeByRange(range => ({ - changes: [ - { from: range.from, insert: '*' }, - { from: range.to, insert: '*' } - ], - range: EditorSelection.range(range.from + 1, range.to + 1) - })) - ) - break - } - - case ToolbarAction.UNORDER_LIST: { - const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) - let mark = '- ' - const matchMark = lineInfo.text.match(/^-/) - if (matchMark && matchMark[0]) { - mark = '' + case ToolbarAction.BOLD: { + view.dispatch( + view.state.changeByRange(range => ({ + changes: [ + { from: range.from, insert: '**' }, + { from: range.to, insert: '**' } + ], + range: EditorSelection.range(range.from + 2, range.to + 2) + })) + ) + break } - view.dispatch({ - changes: { - from: lineInfo.from, - to: lineInfo.to, - insert: `${mark}${lineInfo.text}` - }, - // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), - selection: { anchor: view.state.selection.main.from + mark.length } - }) - break - } - case ToolbarAction.CHECK_LIST: { - const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) - let mark = '- [ ] ' - const matchMark = lineInfo.text.match(/^-\s\[\s\]\s/) - if (matchMark && matchMark[0]) { - mark = '' + case ToolbarAction.ITALIC: { + view.dispatch( + view.state.changeByRange(range => ({ + changes: [ + { from: range.from, insert: '*' }, + { from: range.to, insert: '*' } + ], + range: EditorSelection.range(range.from + 1, range.to + 1) + })) + ) + break } - view.dispatch({ - changes: { - from: lineInfo.from, - to: lineInfo.to, - insert: `${mark}${lineInfo.text}` - }, - // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), - selection: { anchor: view.state.selection.main.from + mark.length } - }) - break - } - case ToolbarAction.CODE_BLOCK: { - const main = view.state.selection.main - const txt = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to) - view.dispatch({ - changes: { - from: main.from, - to: main.to, - insert: `\`\`\`tsx\n${txt}\n\`\`\`` - }, - selection: EditorSelection.range(main.from + 3, main.from + 6) - }) - break + case ToolbarAction.UNORDER_LIST: { + const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) + let mark = '- ' + const matchMark = lineInfo.text.match(/^-/) + if (matchMark && matchMark[0]) { + mark = '' + } + view.dispatch({ + changes: { + from: lineInfo.from, + to: lineInfo.to, + insert: `${mark}${lineInfo.text}` + }, + // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), + selection: { anchor: view.state.selection.main.from + mark.length } + }) + break + } + + case ToolbarAction.CHECK_LIST: { + const lineInfo = view.state.doc.lineAt(view.state.selection.main.from) + let mark = '- [ ] ' + const matchMark = lineInfo.text.match(/^-\s\[\s\]\s/) + if (matchMark && matchMark[0]) { + mark = '' + } + view.dispatch({ + changes: { + from: lineInfo.from, + to: lineInfo.to, + insert: `${mark}${lineInfo.text}` + }, + // selection: EditorSelection.range(lineInfo.from + mark.length, lineInfo.to), + selection: { anchor: view.state.selection.main.from + mark.length } + }) + break + } + + case ToolbarAction.CODE_BLOCK: { + const main = view.state.selection.main + const txt = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to) + + view.dispatch({ + changes: { + from: main.from, + to: main.to, + insert: `\`\`\`tsx\n${txt}\n\`\`\`` + }, + selection: EditorSelection.range(main.from + 3, main.from + 6) + }) + break + } + + case ToolbarAction.SUGGESTION: { + const main = view.state.selection.main + const txt = suggestionBlock?.source || '' + + view.dispatch({ + changes: { + from: main.from, + to: main.to, + insert: `\`\`\`suggestion\n${txt}\n\`\`\`` + } + }) + break + } } - } - }, []) + }, + [suggestionBlock] + ) useEffect(() => { setDirtyProp?.(dirty) @@ -605,8 +645,9 @@ export function MarkdownEditorWithPreview({ size={ButtonSize.SMALL} variation={ButtonVariation.ICON} icon={item.icon} + title={item.title} withoutCurrentColor - iconProps={{ color: Color.PRIMARY_10, size: 14 }} + iconProps={{ color: Color.PRIMARY_10, size: item.size || 14 }} onClick={() => onToolbarAction(item.action)} /> ) @@ -614,7 +655,7 @@ export function MarkdownEditorWithPreview({ {selectedTab === MarkdownEditorTab.PREVIEW && ( - + )} {!standalone && showFeedback && ( = ({ + code, + suggestionBlock, + suggestionCheckSums +}) => { + const { getString } = useStrings() + const codeBlockContent = suggestionBlock?.source || '' + const lang = suggestionBlock?.lang || 'plaintext' + const language = `language-${lang}` + const html1 = toHtml(refractor.highlight(codeBlockContent, lang) as unknown as Nodes) + const html2 = toHtml(refractor.highlight(code, lang) as unknown as Nodes) + const ref = useRef(null) + const suggestionRef = useRef() + const [checksum, setChecksum] = useState('') + + // TODO: Use `fast-diff` to decorate `removed, `added` blocks + // Similar to Github. Otherwise, it looks plain + // https://codesandbox.io/p/sandbox/intelligent-noether-3qd6mj?file=%2Fsrc%2FApp.js%3A1%2C19-1%2C28 + // Flow: + // For removed block: Scan fast diff result, if a removed block is matched, mark bg red + // For added block: Scan fast diff result, if an added block is matched, mark bg green + + // Notes: Since the suggestion checksums are on the comment level (JSON), and the suggestions themselves are + // embedded in the comment content (Text), which make them be nothing related in terms of structure. We need + // a way to link them together: + // 1- Render suggestion block, each being marked with the comment + // 2- When rendering is complete, we query all suggestions block and match each block to its check sum + // by index. + useEffect(() => { + const commentId = suggestionBlock?.commentId + + if (commentId && suggestionCheckSums?.length && ref.current) { + const parent = ref.current.closest(`[data-comment-id="${commentId}"]`) + const suggestionBlockDOMs = parent?.querySelectorAll(`[data-suggestion-comment-id="${commentId}"]`) + let index = 0 + + if (suggestionBlockDOMs?.length) { + while (suggestionBlockDOMs[index]) { + if (suggestionBlockDOMs[index] === ref.current) { + setChecksum(suggestionCheckSums[index]) + break + } + index++ + } + } + } + }, [code, suggestionBlock?.commentId, suggestionCheckSums]) + + const text = useMemo(() => stringSubstitute(getString('pr.commitSuggestions'), { count: 1 }), [getString]) + const [suggestions, setSuggestions] = useAtom(pullReqSuggestionsAtom) + const commitPullReqSuggestions = useCommitPullReqSuggestions() + const [openCommitSuggestionsModal] = useCommitSuggestionsModal({ + title: text as string, + commitMessage: stringSubstitute(getString('pr.applySuggestions'), { count: 1 }) as string, + onCommit: async formData => { + return new Promise(resolve => { + commitPullReqSuggestions({ + bypass_rules: true, + dry_run_rules: false, + title: formData.commitMessage, + message: formData.extendedDescription, + suggestions: [suggestionRef.current] + }) + .then(() => { + resolve(null) + + switch (getActivePullReqPageSection()) { + case PullRequestSection.FILES_CHANGED: + waitUntil({ + test: () => document.querySelector('[data-button-name="refresh-pr"]') as HTMLElement, + onMatched: dom => { + dom?.click?.() + }, + onExpired: () => { + dispatchCustomEvent(PullReqCustomEvent.REFETCH_DIFF, null) + } + }) + break + + case PullRequestSection.CONVERSATION: + // Activities are refetched by SSE event, nothing to do here + break + } + }) + .catch(e => { + resolve(getErrorMessage(e)) + }) + }) + } + }) + + useEffect(() => { + suggestionRef.current = { + check_sum: checksum, + comment_id: suggestionBlock?.commentId as number + } + }, [checksum, suggestionBlock?.commentId]) + + const states = useMemo( + () => ({ + addedToBatch: suggestions?.find(item => item.check_sum === checksum), + otherAddedToBatch: suggestions?.find( + item => item.check_sum !== checksum && item.comment_id === suggestionBlock?.commentId + ) + }), + [suggestions, checksum, suggestionBlock] + ) + + const actions = useMemo( + () => ({ + addToBatch: () => { + setSuggestions([...suggestions, { check_sum: checksum, comment_id: suggestionBlock?.commentId as number }]) + }, + removeFromBatch: () => { + setSuggestions(suggestions.filter(suggestion => suggestion.check_sum !== checksum)) + }, + commit: openCommitSuggestionsModal + }), + [checksum, suggestionBlock, suggestions, setSuggestions, openCommitSuggestionsModal] + ) + + return ( + + + + {getString( + suggestionBlock?.appliedCheckSum && suggestionBlock?.appliedCheckSum === checksum + ? 'pr.suggestionApplied' + : 'pr.suggestedChange' + )} + + + + +
+              
+            
+
+ +
+              
+            
+
+
+ {!!suggestionCheckSums?.length && ( + + + + + + + + ) +} diff --git a/web/src/components/SuggestionBlock/SuggestionBlock.tsx b/web/src/components/SuggestionBlock/SuggestionBlock.tsx new file mode 100644 index 000000000..36cf8cdae --- /dev/null +++ b/web/src/components/SuggestionBlock/SuggestionBlock.tsx @@ -0,0 +1,19 @@ +/** + * SuggestionBlock represents a suggestion block. + */ +export interface SuggestionBlock { + /** Orginal source - lines that have suggestion comment */ + source: string + + /** Language of the diff/file */ + lang?: string + + /** Comment id */ + commentId?: number + + /** Applied check sum */ + appliedCheckSum?: string + + /** Applied commit SHA */ + appliedCommitSha?: string +} diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 38ea9c649..3a4caac98 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -39,6 +39,7 @@ export interface StringsMap { blame: string blameCommitLine: string blameEmpty: string + bold: string botAlerts: string bottom: string branch: string @@ -140,6 +141,7 @@ export interface StringsMap { characterLimit: string checkRuns: string checkSuites: string + checklist: string checks: string clear: string clickHereToDownload: string @@ -148,6 +150,7 @@ export interface StringsMap { cloneText: string close: string closed: string + code: string 'codeOwner.approvalCompleted': string 'codeOwner.changesRequested': string 'codeOwner.title': string @@ -356,6 +359,7 @@ export interface StringsMap { getMyCloneTitle: string gitIgnore: string gitness: string + heading: string help: string hideCommitHistory: string history: string @@ -422,6 +426,7 @@ export interface StringsMap { inactiveBranches: string invalidResponse: string isRequired: string + italic: string key: string 'keywordSearch.sampleQueries.searchForClass': string 'keywordSearch.sampleQueries.searchForFilesWithCMD': string @@ -598,6 +603,8 @@ export interface StringsMap { poweredByAI: string 'pr.ableToMerge': string 'pr.addDescription': string + 'pr.addSuggestion': string + 'pr.applySuggestions': string 'pr.authorCommentedPR': string 'pr.branchHasNoConflicts': string 'pr.cantBeMerged': string @@ -607,6 +614,8 @@ export interface StringsMap { 'pr.checksFailure': string 'pr.collapseFullFile': string 'pr.commentLineNumbers': string + 'pr.commitSuggestion': string + 'pr.commitSuggestions': string 'pr.copyLinkToComment': string 'pr.createDraftPR': string 'pr.descHasTooLongLine': string @@ -657,6 +666,7 @@ export interface StringsMap { 'pr.prStateChanged': string 'pr.prStateChangedDraft': string 'pr.readyForReview': string + 'pr.removeSuggestion': string 'pr.requestSubmitted': string 'pr.reviewChanges': string 'pr.reviewSubmitted': string @@ -666,6 +676,8 @@ export interface StringsMap { 'pr.state': string 'pr.status': string 'pr.statusLine': string + 'pr.suggestedChange': string + 'pr.suggestionApplied': string 'pr.titleChanged': string 'pr.titleChangedTable': string 'pr.titleIsRequired': string @@ -837,6 +849,7 @@ export interface StringsMap { 'stepCategory.select': string submitReview: string success: string + suggestion: string summary: string switchBranch: string switchBranchesTags: string @@ -867,6 +880,7 @@ export interface StringsMap { 'triggers.newTrigger': string 'triggers.updateSuccess': string turnOnSemanticSearch: string + unorderedList: string unrsolvedComment: string 'unsavedChanges.leave': string 'unsavedChanges.message': string @@ -877,6 +891,7 @@ export interface StringsMap { updateUser: string updateWebhook: string updated: string + upload: string uploadAFileError: string user: string userCreated: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index ae03fe347..bed5b89cd 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -242,6 +242,13 @@ diff: Diff draft: Draft conversation: Conversation pr: + suggestedChange: Suggested change + addSuggestion: Add suggestion to batch + removeSuggestion: Remove suggestion from batch + commitSuggestions: 'Commit {count|1:suggestion,suggestions}' + commitSuggestion: Commit suggestion + applySuggestions: 'Apply {count|1:suggestion,suggestions} from code review' + suggestionApplied: Suggestion applied commentLineNumbers: Comment on line {{start}} to {{end}} moreComments: '{num} {count|0:Show more,1:1 more comment,more comments}' copyLinkToComment: Copy link to comment @@ -287,8 +294,8 @@ pr: requestSubmitted: Request for changes submitted. prReviewSubmit: '{user} {state|approved:approved, rejected:rejected,changereq:requested changes to, reviewed} this pull request. {time}' prMergedBannerInfo: '{user} merged branch {source} into {target} {time}.' - prMergedInfo: '{user}{bypassed|true: bypassed rules and , }merged changes from {source} into {target} as {mergeSha} {time}' - prRebasedInfo: '{user}{bypassed|true: bypassed rules and , }rebased changes from branch {source} onto {target}, now at {mergeSha} {time}' + prMergedInfo: '{user} merged changes from {source} into {target} as {mergeSha} {time}' + prRebasedInfo: '{user} rebased changes from branch {source} onto {target}, now at {mergeSha} {time}' prBranchPushInfo: '{user} pushed a new commit {commit}' prBranchDeleteInfo: '{user} deleted the source branch with latest commit {commit}' prStateChanged: '{user} changed pull request state from {old} to {new}.' @@ -1028,3 +1035,11 @@ securitySettings: detectDesc: passive vulnerability will report errors but not block block: Block blockDesc: active vulnerability blocks commit if any vulnerability is found +suggestion: 'Add a suggestion, ' +heading: Heading +bold: Bold +italic: Italic +upload: Upload +unorderedList: Unordered list +checklist: Check list +code: Code diff --git a/web/src/pages/PullRequest/Checks/Checks.tsx b/web/src/pages/PullRequest/Checks/Checks.tsx index 7aaa5112a..7ff61bbc0 100644 --- a/web/src/pages/PullRequest/Checks/Checks.tsx +++ b/web/src/pages/PullRequest/Checks/Checks.tsx @@ -22,7 +22,7 @@ import { useHistory } from 'react-router-dom' import { Container, Layout, Text, FlexExpander, Button, ButtonVariation, ButtonSize } from '@harnessio/uicore' import { Color, FontVariation } from '@harnessio/design-system' import { LogViewer } from 'components/LogViewer/LogViewer' -import { PullRequestCheckType } from 'utils/Utils' +import { PullRequestCheckType, PullRequestSection } from 'utils/Utils' import { useAppContext } from 'AppContext' import { useStrings } from 'framework/strings' import { Split } from 'components/Split/Split' @@ -176,7 +176,7 @@ export const Checks: React.FC = ({ repoMetadata, pullReqMetadata, p } return ( - + diff --git a/web/src/pages/PullRequest/Conversation/CodeCommentHeader.tsx b/web/src/pages/PullRequest/Conversation/CodeCommentHeader.tsx index 7213a8e2c..d13b3bcca 100644 --- a/web/src/pages/PullRequest/Conversation/CodeCommentHeader.tsx +++ b/web/src/pages/PullRequest/Conversation/CodeCommentHeader.tsx @@ -53,8 +53,8 @@ export const CodeCommentHeader: React.FC = ({ `diff --git a/src b/dest`, `new file mode 100644`, 'index 0000000..0000000', - '--- a/src', - '+++ b/dest', + `--- a/src/${get(commentItems[0], 'payload.code_comment.path')}`, + `+++ b/dest/${get(commentItems[0], 'payload.code_comment.path')}`, get(commentItems[0], 'payload.payload.title', ''), ...get(commentItems[0], 'payload.payload.lines', []) ].join('\n') diff --git a/web/src/pages/PullRequest/Conversation/Conversation.tsx b/web/src/pages/PullRequest/Conversation/Conversation.tsx index cdf9cd407..43c9c2812 100644 --- a/web/src/pages/PullRequest/Conversation/Conversation.tsx +++ b/web/src/pages/PullRequest/Conversation/Conversation.tsx @@ -28,15 +28,15 @@ import { } from '@harnessio/uicore' import { useLocation } from 'react-router-dom' import { useGet, useMutate } from 'restful-react' -import { orderBy } from 'lodash-es' +import { get, orderBy } from 'lodash-es' import type { GitInfoProps } from 'utils/GitUtils' import { useStrings } from 'framework/strings' import { useAppContext } from 'AppContext' import type { TypesPullReqActivity, TypesPullReq, TypesPullReqStats, TypesCodeOwnerEvaluation } from 'services/code' import { CommentAction, CommentBox, CommentBoxOutletPosition, CommentItem } from 'components/CommentBox/CommentBox' import { useConfirmAct } from 'hooks/useConfirmAction' -import { getErrorMessage, orderSortDate, ButtonRoleProps, PullRequestSection } from 'utils/Utils' -import { activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils' +import { getErrorMessage, orderSortDate, ButtonRoleProps, PullRequestSection, filenameToLanguage } from 'utils/Utils' +import { activitiesToDiffCommentItems, activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils' import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck' import { ThreadSection } from 'components/ThreadSection/ThreadSection' import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/CodeCommentStatusSelect' @@ -44,6 +44,7 @@ import { CodeCommentStatusButton } from 'components/CodeCommentStatusButton/Code import { CodeCommentSecondarySaveButton } from 'components/CodeCommentSecondarySaveButton/CodeCommentSecondarySaveButton' import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision' import { UserPreference, useUserPreference } from 'hooks/useUserPreference' +import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration' import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper' import { DescriptionBox } from './DescriptionBox' import { PullRequestActionsBox } from './PullRequestActionsBox/PullRequestActionsBox' @@ -254,9 +255,24 @@ export const Conversation: React.FC = ({ }> ) } + + const activity = commentItems[0].payload + const right = get(activity?.payload, 'line_start_new', false) + const span = right ? activity?.code_comment?.span_new || 0 : activity?.code_comment?.span_old || 0 + const startLine = (right ? activity?.code_comment?.line_new : activity?.code_comment?.line_old) as number + const endLine = startLine + span - 1 + + const comment = activitiesToDiffCommentItems(activity?.code_comment?.path as string, [ + activity as TypesPullReqActivity + ])[0] + const suggestionBlock = { + source: comment.codeBlockContent as string, + lang: filenameToLanguage(activity?.code_comment?.path?.split('/').pop()) + } + return ( = ({ enableReplyPlaceHolder={true} autoFocusAndPosition={true} copyLinkToComment={copyLinkToComment} + suggestionBlock={suggestionBlock} handleAction={async (action, value, commentItem) => { let result = true let updatedItem: CommentItem | undefined = undefined @@ -328,12 +345,15 @@ export const Conversation: React.FC = ({ }} outlets={{ [CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]: isCodeComment(commentItems) && ( - + <> + + + ), [CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]: ( = ({ ) }), // eslint-disable-next-line react-hooks/exhaustive-deps - [activityBlocks, currentUser, pullReqMetadata] + [activityBlocks, currentUser, pullReqMetadata, activities] ) return ( - + = ({ + ), panel: ( - + {!!repoMetadata && !!pullReqMetadata && !!pullReqStats && ( { @@ -33,7 +34,7 @@ export const PullRequestCommits: React.FC = ({ const { getString } = useStrings() return ( - + = ({ + section, className, loading, error, @@ -34,7 +36,7 @@ export const PullRequestTabContentWrapper: React.FC { return ( - + {error && } {!error && children} diff --git a/web/src/pages/PullRequest/PullRequestUtils.tsx b/web/src/pages/PullRequest/PullRequestUtils.tsx index 3dbe194be..60a021515 100644 --- a/web/src/pages/PullRequest/PullRequestUtils.tsx +++ b/web/src/pages/PullRequest/PullRequestUtils.tsx @@ -17,6 +17,7 @@ import type { EnumPullReqReviewDecision, TypesPullReqActivity } from 'services/code' import type { CommentItem } from 'components/CommentBox/CommentBox' import { CommentType } from 'components/DiffViewer/DiffViewerUtils' +import type { PullRequestSection } from 'utils/Utils' export function isCodeComment(commentItems: CommentItem[]) { return commentItems[0]?.payload?.type === CommentType.CODE_COMMENT @@ -45,3 +46,12 @@ export const processReviewDecision = ( review_decision === PullReqReviewDecision.approved && reviewedSHA !== sourceSHA ? PullReqReviewDecision.outdated : review_decision + +export function getActivePullReqPageSection(): PullRequestSection | undefined { + return (document.querySelector('[data-page-section]') as HTMLElement)?.dataset?.pageSection as PullRequestSection +} + +export enum PullReqCustomEvent { + REFETCH_DIFF = 'PullReqCustomEvent_REFETCH', + REFETCH_ACTIVITIES = 'PullReqCustomEvent_REFETCH_ACTIVITIES' +} diff --git a/web/src/pages/PullRequest/useGetPullRequestInfo.ts b/web/src/pages/PullRequest/useGetPullRequestInfo.ts index 941bca67c..8141ced61 100644 --- a/web/src/pages/PullRequest/useGetPullRequestInfo.ts +++ b/web/src/pages/PullRequest/useGetPullRequestInfo.ts @@ -218,9 +218,9 @@ export function usePullReqActivities() { return activities } -const pullReqAtom = atom(undefined) +export const pullReqAtom = atom(undefined) const pullReqStatsAtom = atom(undefined) -const pullReqActivitiesAtom = atom(undefined) +export const pullReqActivitiesAtom = atom(undefined) const pullReqCommitsAtom = atom(undefined) // Note: We just list COMMITS_LIMIT commits in PR page diff --git a/web/tsconfig.json b/web/tsconfig.json index 0862119b8..4bc831f1d 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": "./src", "target": "es2018", - "lib": ["dom"], + "lib": ["dom", "es2021"], "module": "esnext", "moduleResolution": "node", "allowJs": true, diff --git a/web/yarn.lock b/web/yarn.lock index 6344960ad..6e8d00600 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2441,6 +2441,13 @@ dependencies: "@types/unist" "*" +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -2541,6 +2548,13 @@ dependencies: "@types/unist" "*" +"@types/mdast@^4.0.0": + version "4.0.3" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" + integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== + dependencies: + "@types/unist" "*" + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -2745,6 +2759,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/unist@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" + integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== + "@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -3143,6 +3162,11 @@ remark-gfm "~3.0.1" unist-util-visit "^4.1.0" +"@ungap/structured-clone@^1.0.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": version "1.11.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" @@ -4121,6 +4145,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + character-entities-legacy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" @@ -4901,6 +4930,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -6531,6 +6567,20 @@ hast-util-from-parse5@^7.0.0: vfile-location "^4.0.0" web-namespaces "^2.0.0" +hast-util-from-parse5@^8.0.0: + version "8.0.1" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" + integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^8.0.0" + property-information "^6.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + hast-util-has-property@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz#8ec99c3e8f02626304ee438cdb9f0528b017e083" @@ -6558,6 +6608,13 @@ hast-util-parse-selector@^3.0.0: dependencies: "@types/hast" "^2.0.0" +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99" @@ -6575,6 +6632,25 @@ hast-util-raw@^7.2.0: web-namespaces "^2.0.0" zwitch "^2.0.0" +hast-util-raw@^9.0.0: + version "9.0.2" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz#39b4a4886bd9f0a5dd42e86d02c966c2c152884c" + integrity sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + hast-util-select@^5.0.5, hast-util-select@~5.0.1: version "5.0.5" resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-5.0.5.tgz#be9ccb71d2278681ca024727f12abd4f93b3e9bc" @@ -6596,6 +6672,24 @@ hast-util-select@^5.0.5, hast-util-select@~5.0.1: unist-util-visit "^4.0.0" zwitch "^2.0.0" +hast-util-to-html@^9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.1.tgz#d108aba473c0ced8377267b1a725b25e818ff3c8" + integrity sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-raw "^9.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + hast-util-to-parse5@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3" @@ -6608,6 +6702,19 @@ hast-util-to-parse5@^7.0.0: web-namespaces "^2.0.0" zwitch "^2.0.0" +hast-util-to-parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + hast-util-to-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a" @@ -6620,6 +6727,13 @@ hast-util-whitespace@^2.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + hastscript@^7.0.0: version "7.2.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" @@ -6631,6 +6745,17 @@ hastscript@^7.0.0: property-information "^6.0.0" space-separated-tokens "^2.0.0" +hastscript@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" + integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -6736,6 +6861,11 @@ html-void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + html-webpack-plugin@^5.3.1: version "5.5.1" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763" @@ -8597,6 +8727,21 @@ mdast-util-to-hast@^12.1.0: unist-util-position "^4.0.0" unist-util-visit "^4.0.0" +mdast-util-to-hast@^13.0.0: + version "13.1.0" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124" + integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" @@ -8839,6 +8984,14 @@ micromark-util-character@^1.0.0: micromark-util-symbol "^1.0.0" micromark-util-types "^1.0.0" +micromark-util-character@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromark-util-chunked@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06" @@ -8885,6 +9038,11 @@ micromark-util-encode@^1.0.0: resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383" integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA== +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + micromark-util-html-tag-name@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz#eb227118befd51f48858e879b7a419fc0df20497" @@ -8913,6 +9071,15 @@ micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: micromark-util-encode "^1.0.0" micromark-util-symbol "^1.0.0" +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-subtokenize@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105" @@ -8928,11 +9095,21 @@ micromark-util-symbol@^1.0.0: resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e" integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ== +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20" integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w== +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + micromark@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.1.0.tgz#eeba0fe0ac1c9aaef675157b52c166f125e89f62" @@ -9705,6 +9882,13 @@ parse5@6.0.1, parse5@^6.0.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -11617,6 +11801,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -12236,6 +12428,13 @@ unist-util-is@^5.0.0: dependencies: "@types/unist" "^2.0.0" +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-position@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" @@ -12243,6 +12442,13 @@ unist-util-position@^4.0.0: dependencies: "@types/unist" "^2.0.0" +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" @@ -12250,6 +12456,13 @@ unist-util-stringify-position@^3.0.0: dependencies: "@types/unist" "^2.0.0" +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: version "5.1.3" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" @@ -12258,6 +12471,14 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: "@types/unist" "^2.0.0" unist-util-is "^5.0.0" +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.2, unist-util-visit@~4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" @@ -12267,6 +12488,15 @@ unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.2, unist unist-util-is "^5.0.0" unist-util-visit-parents "^5.1.1" +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -12479,6 +12709,14 @@ vfile-location@^4.0.0: "@types/unist" "^2.0.0" vfile "^5.0.0" +vfile-location@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" + integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" @@ -12487,6 +12725,14 @@ vfile-message@^3.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^3.0.0" +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile@^5.0.0: version "5.3.7" resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" @@ -12497,6 +12743,15 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +vfile@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + vscode-languageserver-textdocument@^1.0.0: version "1.0.8" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" @@ -13035,7 +13290,7 @@ yup@^0.29.1: synchronous-promise "^2.0.13" toposort "^2.0.2" -zwitch@^2.0.0: +zwitch@^2.0.0, zwitch@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==