Pull Request Code suggestions (#2032)

pull/3519/head
Tan Nhu 2024-05-13 18:35:07 +00:00 committed by Harness
parent ea205ff7ba
commit 44d1a82e6f
31 changed files with 1318 additions and 234 deletions

View File

@ -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",

View File

@ -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<Suggestion[]>([])

View File

@ -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<ChangesProps> = ({
[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<ChangesProps> = ({
}, [diffs, setPullReqChangesCount])
useShowRequestError(errorFileViews, 0)
return (
<Container className={cx(css.container, className)} {...(!!loadingRawDiff || !!error ? { flex: true } : {})}>
<LoadingSpinner visible={loading} withBorder={true} />
@ -471,13 +484,8 @@ const ChangesInternal: React.FC<ChangesProps> = ({
<PlainButton
text={getString('refresh')}
className={css.refreshBtn}
onClick={() => {
setCachedDiff({})
setTargetRef(_targetRef)
setSourceRef(_sourceRef)
setPrHasChanged(false)
refetchCommits?.()
}}
onClick={refreshPullReq}
data-button-name="refresh-pr"
/>
</Render>
@ -497,13 +505,19 @@ const ChangesInternal: React.FC<ChangesProps> = ({
<FlexExpander />
<ReviewSplitButton
shouldHide={shouldHideReviewButton}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
refreshPr={voidFn(noop)}
disabled={isActiveUserPROwner}
/>
<Container flex={{ alignItems: 'center' }}>
<Layout.Horizontal spacing="medium">
<PullReqSuggestionsBatch />
<ReviewSplitButton
shouldHide={shouldHideReviewButton}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
refreshPr={voidFn(noop)}
disabled={isActiveUserPROwner}
/>
</Layout.Horizontal>
</Container>
</Layout.Horizontal>
</Container>
</Render>

View File

@ -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);

View File

@ -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<T = unknown> {
@ -108,6 +96,7 @@ interface CommentBoxProps<T> {
standalone: boolean
routingId: string
copyLinkToComment: (commentId: number, commentItem: CommentItem<T>) => void
suggestionBlock?: SuggestionBlock
}
const CommentBoxInternal = <T = unknown,>({
@ -131,7 +120,8 @@ const CommentBoxInternal = <T = unknown,>({
repoMetadata,
standalone,
routingId,
copyLinkToComment
copyLinkToComment,
suggestionBlock
}: CommentBoxProps<T>) => {
const { getString } = useStrings()
const [comments, setComments] = useState<CommentItem<T>[]>(commentItems)
@ -217,6 +207,7 @@ const CommentBoxInternal = <T = unknown,>({
}}
outlets={outlets}
copyLinkToComment={copyLinkToComment}
suggestionBlock={suggestionBlock}
/>
<Match expr={showReplyPlaceHolder && enableReplyPlaceHolderRef.current}>
<Truthy>
@ -298,6 +289,7 @@ const CommentBoxInternal = <T = unknown,>({
setDirties({ ...dirties, ['new']: _dirty })
}}
autoFocusAndPosition={autoFocusAndPosition ? !showReplyPlaceHolder : false}
suggestionBlock={suggestionBlock}
/>
</Container>
</Falsy>
@ -310,7 +302,10 @@ const CommentBoxInternal = <T = unknown,>({
}
interface CommentsThreadProps<T>
extends Pick<CommentBoxProps<T>, 'commentItems' | 'handleAction' | 'outlets' | 'copyLinkToComment'> {
extends Pick<
CommentBoxProps<T>,
'commentItems' | 'handleAction' | 'outlets' | 'copyLinkToComment' | 'suggestionBlock'
> {
onQuote: (content: string) => void
setDirty: (index: number, dirty: boolean) => void
repoMetadata: TypesRepository | undefined
@ -323,7 +318,8 @@ const CommentsThread = <T = unknown,>({
setDirty,
outlets = {},
repoMetadata,
copyLinkToComment
copyLinkToComment,
suggestionBlock
}: CommentsThreadProps<T>) => {
const { getString } = useStrings()
const { standalone, routingId } = useAppContext()
@ -335,22 +331,22 @@ const CommentsThread = <T = unknown,>({
},
[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 (
<Render when={commentItems.length}>
<Container className={css.viewer} padding="xlarge">
{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 = <T = unknown,>({
<ThreadSection
key={index}
title={
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
<Layout.Horizontal
spacing="small"
style={{ alignItems: 'center' }}
data-outdated={commentItem?.outdated}>
<Text inline icon="code-chat"></Text>
<Avatar name={commentItem?.author} size="small" hoverCard={false} />
<Text inline>
@ -444,7 +443,7 @@ const CommentsThread = <T = unknown,>({
</Layout.Horizontal>
</Layout.Horizontal>
}
hideGutter={isLastItem || (collapseResolvedComments && collapsed)}>
hideGutter={isLastItem /*|| (collapseResolvedComments && collapsed)*/}>
<Container padding={{ bottom: isLastItem ? undefined : 'xsmall' }} data-comment-id={commentItem.id}>
<Render when={index === 0 && outlets[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]}>
<Container className={css.outletTopOfFirstOfComment}>
@ -478,6 +477,7 @@ const CommentsThread = <T = unknown,>({
cancel: getString('cancel')
}}
autoFocusAndPosition
suggestionBlock={suggestionBlock}
/>
</Container>
</Truthy>
@ -489,10 +489,23 @@ const CommentsThread = <T = unknown,>({
<Else>
<MarkdownViewer
source={
collapseResolvedComments && collapsed
? shorten(commentItem?.content)
: commentItem?.content
/* collapseResolvedComments && collapsed
? shorten(commentItem?.content)
:*/ commentItem?.content
}
suggestionBlock={Object.assign(
{
commentId: commentItem.id,
appliedCheckSum: get(commentItem, 'payload.metadata.suggestions.applied_check_sum', ''),
appliedCommitSha: get(
commentItem,
'payload.metadata.suggestions.applied_commit_sha',
''
)
},
suggestionBlock
)}
suggestionCheckSums={get(commentItem, 'payload.metadata.suggestions.check_sums', [])}
/>
</Else>
</Match>
@ -503,7 +516,7 @@ const CommentsThread = <T = unknown,>({
)
})}
<Render when={shouldCollapsedResolvedComments}>
{/* <Render when={shouldCollapsedResolvedComments}>
<Container
flex={{ justifyContent: 'space-around' }}
padding={{ bottom: 'xsmall' }}
@ -538,7 +551,7 @@ const CommentsThread = <T = unknown,>({
</Button>
</Layout.Horizontal>
</Container>
</Render>
</Render> */}
</Container>
</Render>
)
@ -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'

View File

@ -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 ? (
<Text
color={Color.GREY_500}
padding={{ bottom: 'small' }}
font={{ variation: FontVariation.BODY }}
data-start-line={startLine}
data-end-line={endLine}>
{getString('pr.commentLineNumbers', { start: startLine, end: endLine })}
</Text>
) : null
}

View File

@ -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<Nullable<string>>
}
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 (
<Dialog
isOpen
enforceFocus={false}
onClose={hideModal}
title={''}
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
<Layout.Vertical className={cx(css.main)}>
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
{title || getString('commitChanges')}
</Heading>
<Container margin={{ right: 'xxlarge' }}>
<Formik<FormData>
initialValues={{
commitMessage,
extendedDescription
}}
formName="commitChanges"
enableReinitialize={true}
validateOnChange
validateOnBlur
onSubmit={onSubmit}>
<FormikForm>
<FormInput.Text
name="commitMessage"
label={getString('commitMessage')}
placeholder={commitMessage}
inputGroup={{ autoFocus: true }}
/>
<FormInput.TextArea
className={css.extendedDescription}
name="extendedDescription"
placeholder={extendedDescription || getString('optionalExtendedDescription')}
/>
<Layout.Horizontal spacing="small" padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}>
<Button
type="submit"
variation={ButtonVariation.PRIMARY}
text={getString('commit')}
disabled={loading}
/>
<Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Formik>
</Container>
</Layout.Vertical>
</Dialog>
)
}
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
}

View File

@ -67,6 +67,7 @@ export interface DiffCommentItem<T = Unknown> {
commentItems: CommentItem<T>[]
_commentItems?: CommentItem<T>[]
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
}
}) || []
)

View File

@ -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<TypesPullReqActivity> | undefined = undefined
@ -415,7 +426,9 @@ export function usePullReqComments({
return [result, updatedItem]
}}
outlets={{
[CommentBoxOutletPosition.TOP]: <CommentThreadTopDecoration comment={comment} />,
[CommentBoxOutletPosition.TOP]: (
<CommentThreadTopDecoration startLine={comment.lineNumberStart} endLine={comment.lineNumberEnd} />
),
[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]: (
<CodeCommentStatusSelect
repoMetadata={repoMetadata}
@ -445,6 +458,7 @@ export function usePullReqComments({
)
},
[
diff,
comments,
contentRef,
viewStyle,
@ -455,9 +469,6 @@ export function usePullReqComments({
currentUser?.display_name,
currentUser?.email,
pullReqMetadata,
diff.isRename,
diff.oldName,
diff.filePath,
sourceRef,
targetRef,
save,
@ -921,15 +932,4 @@ function isDiffRendered(ref: React.RefObject<HTMLDivElement | null>) {
return !!ref.current?.querySelector('[data]' || !!ref.current?.querySelector('.d2h-wrapper'))
}
const CommentThreadTopDecoration: React.FC<{ comment: DiffCommentItem<TypesPullReqActivity> }> = ({ comment }) => {
const { getString } = useStrings()
const { lineNumberStart: start, lineNumberEnd: end } = comment
return start !== end ? (
<Text color={Color.GREY_500} padding={{ bottom: 'small' }} font={{ variation: FontVariation.BODY }}>
{getString('pr.commentLineNumbers', { start, end })}
</Text>
) : null
}
const selected = 'selected'

View File

@ -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<HTMLInputElement>(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({
</Container>
<Container className={css.tabContent}>
<Editor
extensions={[myKeymap, decorationField, history()]}
extensions={[shortcuts, decorationField, history()]}
routingId={routingId}
standalone={standalone}
repoMetadata={repoMetadata}
@ -633,7 +674,11 @@ export function MarkdownEditorWithPreview({
}}
/>
{selectedTab === MarkdownEditorTab.PREVIEW && (
<MarkdownViewer source={viewRef.current?.state.doc.toString() || ''} maxHeight={800} />
<MarkdownViewer
source={viewRef.current?.state.doc.toString() || ''}
maxHeight={800}
suggestionBlock={suggestionBlock}
/>
)}
{!standalone && showFeedback && (
<Container

View File

@ -0,0 +1,227 @@
/*
* 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 { Button, ButtonVariation, Container, Layout, Text, Utils, stringSubstitute } from '@harnessio/uicore'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useAtom } from 'jotai'
import { refractor } from 'refractor'
import { Else, Match, Truthy } from 'react-jsx-match'
import { toHtml } from 'hast-util-to-html'
import type { Nodes } from 'hast-util-to-html/lib'
import { useStrings } from 'framework/strings'
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
import { Suggestion, pullReqSuggestionsAtom } from 'atoms/pullReqSuggestions'
import {
useCommitPullReqSuggestions,
useCommitSuggestionsModal
} from 'components/CommitModalButton/useCommitSuggestionModal'
import { PullRequestSection, getErrorMessage, waitUntil } from 'utils/Utils'
import { PullReqCustomEvent, getActivePullReqPageSection } from 'pages/PullRequest/PullRequestUtils'
import { dispatchCustomEvent } from 'hooks/useEventListener'
import css from './MarkdownViewer.module.scss'
interface CodeSuggestionBlockProps {
code: string
suggestionBlock?: SuggestionBlock
suggestionCheckSums?: string[]
}
//
// NOTE: Adding this component to MarkdownViewer is not ideal as
// it makes MarkdownViewer less independent. It'd be better to adopt
// concept such as Outlet to make the Code Suggestion business on its own
//
export const CodeSuggestionBlock: React.FC<CodeSuggestionBlockProps> = ({
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<HTMLDivElement>(null)
const suggestionRef = useRef<Suggestion>()
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 (
<Container
ref={ref}
className={css.suggestion}
onClick={Utils.stopEvent}
data-suggestion-comment-id={suggestionBlock?.commentId}>
<Layout.Vertical>
<Text className={css.text}>
{getString(
suggestionBlock?.appliedCheckSum && suggestionBlock?.appliedCheckSum === checksum
? 'pr.suggestionApplied'
: 'pr.suggestedChange'
)}
</Text>
<Container>
<Container className={css.removed}>
<pre className={language}>
<code className={`${language} code-highlight`} dangerouslySetInnerHTML={{ __html: html1 }}></code>
</pre>
</Container>
<Container className={css.added}>
<pre className={language}>
<code className={`${language} code-highlight`} dangerouslySetInnerHTML={{ __html: html2 }}></code>
</pre>
</Container>
</Container>
{!!suggestionCheckSums?.length && (
<Container data-section-id="CodeSuggestionBlockButtons">
<Layout.Horizontal spacing="small" padding="medium">
<Match expr={states.addedToBatch}>
<Truthy>
<Button
intent="danger"
variation={ButtonVariation.SECONDARY}
text={getString('pr.removeSuggestion')}
onClick={actions.removeFromBatch}
/>
</Truthy>
<Else>
<Button
variation={ButtonVariation.TERTIARY}
text={getString('pr.addSuggestion')}
onClick={actions.addToBatch}
disabled={!!states.otherAddedToBatch}
/>
<Button
variation={ButtonVariation.TERTIARY}
text={getString('pr.commitSuggestion')}
onClick={actions.commit}
disabled={!!states.otherAddedToBatch}
/>
</Else>
</Match>
</Layout.Horizontal>
</Container>
)}
</Layout.Vertical>
</Container>
)
}

View File

@ -66,3 +66,32 @@
}
}
}
.suggestion {
background-color: var(--white) !important;
border: 1px solid var(--grey-200);
border-radius: 4px;
pre {
margin-bottom: 0 !important;
border-radius: 0 !important;
}
.removed pre {
background-color: var(--red-100) !important;
}
.added pre {
background-color: var(--green-100) !important;
}
.text {
color: var(--grey-500);
font-size: 11px !important;
padding: var(--spacing-small) !important;
}
}
.suggestion + [data-code] {
display: none !important;
}

View File

@ -16,5 +16,9 @@
/* eslint-disable */
// This is an auto-generated file
export declare const added: string
export declare const main: string
export declare const removed: string
export declare const suggestion: string
export declare const text: string
export declare const withMaxHeight: string

View File

@ -15,15 +15,18 @@
*/
import { useHistory } from 'react-router-dom'
import { Container, Utils } from '@harnessio/uicore'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Container } from '@harnessio/uicore'
import { isEmpty } from 'lodash-es'
import cx from 'classnames'
import { getCodeString } from 'rehype-rewrite'
import MarkdownPreview from '@uiw/react-markdown-preview'
import rehypeVideo from 'rehype-video'
import rehypeExternalLinks, { Element } from 'rehype-external-links'
import { INITIAL_ZOOM_LEVEL, generateAlphaNumericHash } from 'utils/Utils'
import ImageCarousel from 'components/ImageCarousel/ImageCarousel'
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
import { CodeSuggestionBlock } from './CodeSuggestionBlock'
import css from './MarkdownViewer.module.scss'
interface MarkdownViewerProps {
@ -34,6 +37,8 @@ interface MarkdownViewerProps {
darkMode?: boolean
handleDescUpdate?: (payload: string) => void
setOriginalContent?: React.Dispatch<React.SetStateAction<string>>
suggestionBlock?: SuggestionBlock
suggestionCheckSums?: string[]
}
export function MarkdownViewer({
@ -41,10 +46,11 @@ export function MarkdownViewer({
className,
maxHeight,
darkMode,
setOriginalContent,
handleDescUpdate,
inDescriptionBox = false
inDescriptionBox = false,
suggestionBlock,
suggestionCheckSums
}: MarkdownViewerProps) {
const [isOpen, setIsOpen] = useState<boolean>(false)
const history = useHistory()
@ -203,6 +209,32 @@ export function MarkdownViewer({
[rehypeVideo, { test: /\/(.*)(.mp4|.mov|.webm|.mkv|.flv)$/, details: null }],
[rehypeExternalLinks, { rel: ['nofollow noreferrer noopener'], target: '_blank' }]
]}
components={{
// Rewriting the code component to support code suggestions
code: ({ children = [], className: _className, ...props }) => {
const code = props.node && props.node.children ? getCodeString(props.node.children) : children
if (
typeof code === 'string' &&
typeof _className === 'string' &&
/^language-suggestion/.test(_className.toLocaleLowerCase())
) {
return (
<CodeSuggestionBlock
code={code}
suggestionBlock={suggestionBlock}
suggestionCheckSums={suggestionCheckSums}
/>
)
}
return (
<code onClick={Utils.stopEvent} className={String(_className)}>
{children}
</code>
)
}
}}
/>
<ImageCarousel
isOpen={isOpen}

View File

@ -0,0 +1,11 @@
.count {
display: inline-block;
margin-left: var(--spacing-small);
display: inline-block;
border-radius: 8px;
font-weight: 500;
font-size: var(--font-size-small);
color: var(--white) !important;
background-color: var(--primary-7) !important;
padding: 3px 6px !important;
}

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const count: string

View File

@ -0,0 +1,98 @@
/*
* 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, { useEffect, useMemo, useRef } from 'react'
import { Button, ButtonVariation, Container, stringSubstitute } from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import { useAtom } from 'jotai'
import { Suggestion, pullReqSuggestionsAtom } from 'atoms/pullReqSuggestions'
import { useStrings } from 'framework/strings'
import {
useCommitPullReqSuggestions,
useCommitSuggestionsModal
} from 'components/CommitModalButton/useCommitSuggestionModal'
import { PullRequestSection, getErrorMessage, waitUntil } from 'utils/Utils'
import { dispatchCustomEvent } from 'hooks/useEventListener'
import { PullReqCustomEvent, getActivePullReqPageSection } from 'pages/PullRequest/PullRequestUtils'
import css from './PullReqSuggestionsBatch.module.scss'
export const PullReqSuggestionsBatch: React.FC = () => {
const [suggestions, setSuggestions] = useAtom(pullReqSuggestionsAtom)
const suggestionsRef = useRef<Suggestion[]>(suggestions)
const { getString } = useStrings()
const text = useMemo(
() => stringSubstitute(getString('pr.commitSuggestions'), { count: suggestions?.length }),
[suggestions, getString]
)
const commitPullReqSuggestions = useCommitPullReqSuggestions()
const [openCommitSuggestionsModal] = useCommitSuggestionsModal({
title: text as string,
commitMessage: stringSubstitute(getString('pr.applySuggestions'), { count: suggestions?.length }) as string,
onCommit: async formData => {
return new Promise(resolve => {
commitPullReqSuggestions({
bypass_rules: true,
dry_run_rules: false,
title: formData.commitMessage,
message: formData.extendedDescription,
suggestions: suggestionsRef.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(() => {
suggestionsRef.current = suggestions
}, [suggestions])
useEffect(() => {
setSuggestions([])
}, [setSuggestions])
return (
<Render when={suggestions?.length}>
<Container flex={{ alignItems: 'center' }}>
<Button variation={ButtonVariation.TERTIARY} text={text} onClick={openCommitSuggestionsModal}>
<span className={css.count}>{suggestions?.length}</span>
</Button>
</Container>
</Render>
)
}

View File

@ -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
}

View File

@ -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

View File

@ -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, <Ctrl-g>'
heading: Heading
bold: Bold
italic: Italic
upload: Upload
unorderedList: Unordered list
checklist: Check list
code: Code

View File

@ -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<ChecksProps> = ({ repoMetadata, pullReqMetadata, p
}
return (
<Container className={css.main}>
<Container className={css.main} data-page-section={PullRequestSection.CHECKS}>
<Match expr={prChecksDecisionResult?.overallStatus}>
<Truthy>
<Split split="vertical" size={400} minSize={300} maxSize={700} primary="first">

View File

@ -53,8 +53,8 @@ export const CodeCommentHeader: React.FC<CodeCommentHeaderProps> = ({
`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')

View File

@ -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<ConversationProps> = ({
}></ThreadSection>
)
}
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 (
<ThreadSection
key={`comment-${threadId}`}
key={`comment-${threadId}-${activity?.created}-${activity?.edited}-${activity?.resolved}-${activity?.code_comment?.outdated}`}
onlyTitle={
activityBlocks[index + 1] !== undefined && isSystemComment(activityBlocks[index + 1]) ? true : false
}
@ -278,6 +294,7 @@ export const Conversation: React.FC<ConversationProps> = ({
enableReplyPlaceHolder={true}
autoFocusAndPosition={true}
copyLinkToComment={copyLinkToComment}
suggestionBlock={suggestionBlock}
handleAction={async (action, value, commentItem) => {
let result = true
let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined
@ -328,12 +345,15 @@ export const Conversation: React.FC<ConversationProps> = ({
}}
outlets={{
[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]: isCodeComment(commentItems) && (
<CodeCommentHeader
commentItems={commentItems}
threadId={threadId}
repoMetadata={repoMetadata}
pullReqMetadata={pullReqMetadata}
/>
<>
<CommentThreadTopDecoration startLine={startLine} endLine={endLine} />
<CodeCommentHeader
commentItems={commentItems}
threadId={threadId}
repoMetadata={repoMetadata}
pullReqMetadata={pullReqMetadata}
/>
</>
),
[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]: (
<CodeCommentStatusSelect
@ -363,11 +383,11 @@ export const Conversation: React.FC<ConversationProps> = ({
)
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[activityBlocks, currentUser, pullReqMetadata]
[activityBlocks, currentUser, pullReqMetadata, activities]
)
return (
<PullRequestTabContentWrapper>
<PullRequestTabContentWrapper section={PullRequestSection.CONVERSATION}>
<Container>
<Layout.Vertical spacing="xlarge">
<PullRequestActionsBox

View File

@ -55,6 +55,7 @@ import {
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import RuleViolationAlertModal from 'components/RuleViolationAlertModal/RuleViolationAlertModal'
import { PullReqSuggestionsBatch } from 'components/PullReqSuggestionsBatch/PullReqSuggestionsBatch'
import css from './PullRequestActionsBox.module.scss'
const codeOwnersNotFoundMessage = 'CODEOWNERS file not found'
@ -330,6 +331,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
<Render when={loading || loadingState}>
<Icon name={CodeIcon.InputSpinner} size={16} margin={{ right: 'xsmall' }} />
</Render>
<PullReqSuggestionsBatch />
<Match expr={isDraft}>
<Truthy>
<SplitButton

View File

@ -189,7 +189,7 @@ export default function PullRequest() {
/>
),
panel: (
<Container className={css.changes}>
<Container className={css.changes} data-page-section={PullRequestSection.FILES_CHANGED}>
{!!repoMetadata && !!pullReqMetadata && !!pullReqStats && (
<Changes
repoMetadata={repoMetadata}

View File

@ -19,6 +19,7 @@ import type { TypesListCommitResponse } from 'services/code'
import type { GitInfoProps } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import { CommitsView } from 'components/CommitsView/CommitsView'
import { PullRequestSection } from 'utils/Utils'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
interface PullRequestCommitsProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullReqMetadata'> {
@ -33,7 +34,7 @@ export const PullRequestCommits: React.FC<PullRequestCommitsProps> = ({
const { getString } = useStrings()
return (
<PullRequestTabContentWrapper>
<PullRequestTabContentWrapper section={PullRequestSection.COMMITS}>
<CommitsView
commits={pullReqCommits?.commits || []}
repoMetadata={repoMetadata}

View File

@ -16,10 +16,11 @@
import React from 'react'
import { Container, PageError } from '@harnessio/uicore'
import { getErrorMessage } from 'utils/Utils'
import { PullRequestSection, getErrorMessage } from 'utils/Utils'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
interface PullRequestTabContentWrapperProps {
section: PullRequestSection
className?: string
loading?: boolean
error?: Unknown
@ -27,6 +28,7 @@ interface PullRequestTabContentWrapperProps {
}
export const PullRequestTabContentWrapper: React.FC<PullRequestTabContentWrapperProps> = ({
section,
className,
loading,
error,
@ -34,7 +36,7 @@ export const PullRequestTabContentWrapper: React.FC<PullRequestTabContentWrapper
children
}) => {
return (
<Container className={className} padding="xlarge">
<Container className={className} padding="xlarge" data-page-section={section}>
<LoadingSpinner visible={loading} withBorder={true} />
{error && <PageError message={getErrorMessage(error)} onClick={onRetry} />}
{!error && children}

View File

@ -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<TypesPullReqActivity>[]) {
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'
}

View File

@ -218,9 +218,9 @@ export function usePullReqActivities() {
return activities
}
const pullReqAtom = atom<TypesPullReq | undefined>(undefined)
export const pullReqAtom = atom<TypesPullReq | undefined>(undefined)
const pullReqStatsAtom = atom<TypesPullReqStats | undefined>(undefined)
const pullReqActivitiesAtom = atom<TypesPullReqActivity[] | undefined>(undefined)
export const pullReqActivitiesAtom = atom<TypesPullReqActivity[] | undefined>(undefined)
const pullReqCommitsAtom = atom<TypesListCommitResponse | undefined>(undefined)
// Note: We just list COMMITS_LIMIT commits in PR page

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": "./src",
"target": "es2018",
"lib": ["dom"],
"lib": ["dom", "es2021"],
"module": "esnext",
"moduleResolution": "node",
"allowJs": true,

View File

@ -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==