From fd9fe4dba0dbb7c359613131e64e84110ea75a09 Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Tue, 29 Nov 2022 17:26:51 +0100 Subject: SONAR-17592 Code highlighting for issue messages and code locations --- server/sonar-web/src/main/js/app/theme.js | 3 + .../main/js/apps/issues/components/IssueHeader.tsx | 8 +- .../issues/conciseIssuesList/ConciseIssueBox.tsx | 6 +- .../__snapshots__/ConciseIssueBox-test.tsx.snap | 8 +- .../sonar-web/src/main/js/apps/issues/styles.css | 12 ++- .../SourceViewer/components/LineCode.tsx | 25 ++++- .../__tests__/__snapshots__/LineCode-test.tsx.snap | 6 +- .../SourceViewer/helpers/issueLocations.ts | 1 + .../main/js/components/common/LocationMessage.css | 3 +- .../src/main/js/components/controls/Tooltip.css | 4 + .../src/main/js/components/issue/Issue.css | 9 ++ .../main/js/components/issue/IssueMessageBox.tsx | 6 +- .../components/issue/IssueMessageHighlighting.tsx | 102 +++++++++++++++++++++ .../__tests__/IssueMessageHighlighting-test.tsx | 74 +++++++++++++++ .../IssueMessageHighlighting-test.tsx.snap | 100 ++++++++++++++++++++ .../components/issue/components/IssueMessage.tsx | 18 +++- .../components/issue/components/IssueTitleBar.tsx | 1 + .../__snapshots__/IssueMessage-test.tsx.snap | 24 +++-- .../locations/CrossFileLocationNavigator.tsx | 24 ++++- .../src/main/js/components/locations/FlowsList.tsx | 1 + .../main/js/components/locations/LocationsList.tsx | 3 +- .../locations/SingleFileLocationNavigator.tsx | 9 +- .../__snapshots__/LocationsList-test.tsx.snap | 2 +- .../SingleFileLocationsNavigator-test.tsx.snap | 12 ++- server/sonar-web/src/main/js/types/issues.ts | 23 ++++- server/sonar-web/src/main/js/types/types.ts | 4 + 26 files changed, 453 insertions(+), 35 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/issue/IssueMessageHighlighting.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/__tests__/IssueMessageHighlighting-test.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueMessageHighlighting-test.tsx.snap diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index f84b78a6ebd..4882dcc8eed 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -181,8 +181,11 @@ module.exports = { neutral800: '#333333', white: '#FFFFFF', + whitea18: 'rgba(255, 255, 255, 0.18)', + whitea60: 'rgba(255, 255, 255, 0.60)', black: '#000000', + blacka06: 'rgba(0, 0, 0, 0.06)', blacka38: 'rgba(0, 0, 0, 0.38)', blacka60: 'rgba(0, 0, 0, 0.60)', blacka75: 'rgba(0, 0, 0, 0.75)', diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx index 243ac925d1d..3286e4f6360 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx @@ -25,6 +25,7 @@ import { updateIssue } from '../../../components/issue/actions'; import IssueActionsBar from '../../../components/issue/components/IssueActionsBar'; import IssueChangelog from '../../../components/issue/components/IssueChangelog'; import IssueMessageTags from '../../../components/issue/components/IssueMessageTags'; +import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; @@ -136,7 +137,12 @@ export default class IssueHeader extends React.PureComponent { <>

- {issue.message} + + + { innerRef={(node) => (this.messageElement = node)} onClick={this.handleClick} > - {issue.message} +
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap index d1fc86dd005..bf45b6e28e5 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap @@ -11,7 +11,9 @@ exports[`should render correctly 1`] = ` innerRef={[Function]} onClick={[Function]} > - Reduce the number of conditional operators (4) used in the expression +
- Reduce the number of conditional operators (4) used in the expression +
loc.index === marker); - const message = loc && loc.text; - renderedTokens.push(this.renderMarker(marker, message, selected, leadingMarker)); + const message = loc?.text; + const messageFormattings = loc?.textFormatting; + renderedTokens.push( + this.renderMarker(marker, message, messageFormattings, selected, leadingMarker) + ); }); } renderedTokens.push( @@ -112,12 +117,24 @@ export default class LineCode extends React.PureComponent onLocationSelect(index) : undefined; return ( - + + } + placement="top" + > + } placement="top" > - {issue.message} +
); } diff --git a/server/sonar-web/src/main/js/components/issue/IssueMessageHighlighting.tsx b/server/sonar-web/src/main/js/components/issue/IssueMessageHighlighting.tsx new file mode 100644 index 00000000000..df82725fc69 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/IssueMessageHighlighting.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import classNames from 'classnames'; +import * as React from 'react'; +import { MessageFormatting, MessageFormattingType } from '../../types/issues'; + +export interface IssueMessageHighlightingProps { + message?: string; + messageFormattings?: MessageFormatting[]; +} + +export function IssueMessageHighlighting(props: IssueMessageHighlightingProps) { + const { message, messageFormattings } = props; + + if (!message) { + return null; + } + + if (!(messageFormattings && messageFormattings.length > 0)) { + return <>{message}; + } + + let previousEnd = 0; + + const sanitizedFormattings = [...messageFormattings] + .sort((a, b) => a.start - b.start) + .reduce((acc, messageFormatting) => { + const { type } = messageFormatting; + + if (type !== MessageFormattingType.CODE) { + return acc; + } + + const { start } = messageFormatting; + let { end } = messageFormatting; + + end = Math.min(message.length, end); + + if (start < 0 || end === start || end < start) { + return acc; + } + + if (acc.length > 0) { + const { start: previousStart, end: previousEnd } = acc[acc.length - 1]; + + if (start <= previousEnd) { + acc[acc.length - 1] = { + start: previousStart, + end: Math.max(previousEnd, end), + type, + }; + + return acc; + } + } + + acc.push({ start, end, type }); + + return acc; + }, [] as typeof messageFormattings); + + return ( + + {sanitizedFormattings.map(({ start, end, type }) => { + const beginning = previousEnd; + previousEnd = end; + + return ( + + {message.slice(beginning, start)} + + {message.slice(start, end)} + + + ); + })} + + {message.slice(previousEnd)} + + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/IssueMessageHighlighting-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/IssueMessageHighlighting-test.tsx new file mode 100644 index 00000000000..9a45e3fa72e --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/__tests__/IssueMessageHighlighting-test.tsx @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; +import { MessageFormattingType } from '../../../types/issues'; +import { + IssueMessageHighlighting, + IssueMessageHighlightingProps, +} from '../IssueMessageHighlighting'; + +it.each([ + [undefined, undefined], + ['message', undefined], + ['message', []], + ['message', [{ start: 1, end: 4, type: 'something else' as MessageFormattingType }]], + [ + 'message', + [ + { start: 5, end: 6, type: MessageFormattingType.CODE }, + { start: 1, end: 4, type: MessageFormattingType.CODE }, + ], + ], + [ + 'a somewhat longer message with overlapping ranges', + [{ start: -1, end: 1, type: MessageFormattingType.CODE }], + ], + [ + 'a somewhat longer message with overlapping ranges', + [{ start: 48, end: 70, type: MessageFormattingType.CODE }], + ], + [ + 'a somewhat longer message with overlapping ranges', + [{ start: 0, end: 0, type: MessageFormattingType.CODE }], + ], + [ + 'a somewhat longer message with overlapping ranges', + [ + { start: 11, end: 17, type: MessageFormattingType.CODE }, + { start: 2, end: 25, type: MessageFormattingType.CODE }, + { start: 25, end: 2, type: MessageFormattingType.CODE }, + ], + ], + [ + 'a somewhat longer message with overlapping ranges', + [ + { start: 18, end: 30, type: MessageFormattingType.CODE }, + { start: 2, end: 25, type: MessageFormattingType.CODE }, + ], + ], +])('should format the string with highlights', (message, messageFormattings) => { + const { asFragment } = renderIssueMessageHighlighting({ message, messageFormattings }); + expect(asFragment()).toMatchSnapshot(); +}); + +function renderIssueMessageHighlighting(props: Partial = {}) { + return renderComponent(); +} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueMessageHighlighting-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueMessageHighlighting-test.tsx.snap new file mode 100644 index 00000000000..00802d08bc0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueMessageHighlighting-test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should format the string with highlights 1`] = ``; + +exports[`should format the string with highlights 2`] = ` + + message + +`; + +exports[`should format the string with highlights 3`] = ` + + message + +`; + +exports[`should format the string with highlights 4`] = ` + + + message + + +`; + +exports[`should format the string with highlights 5`] = ` + + + m + + ess + + a + + g + + e + + +`; + +exports[`should format the string with highlights 6`] = ` + + + a somewhat longer message with overlapping ranges + + +`; + +exports[`should format the string with highlights 7`] = ` + + + a somewhat longer message with overlapping range + + s + + + +`; + +exports[`should format the string with highlights 8`] = ` + + + a somewhat longer message with overlapping ranges + + +`; + +exports[`should format the string with highlights 9`] = ` + + + a + + somewhat longer message + + with overlapping ranges + + +`; + +exports[`should format the string with highlights 10`] = ` + + + a + + somewhat longer message with + + overlapping ranges + + +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx index c692a2f01ae..2b2b0519867 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx @@ -20,8 +20,10 @@ import * as React from 'react'; import { ButtonLink } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; +import { MessageFormatting } from '../../../types/issues'; import { RuleStatus } from '../../../types/rules'; import { WorkspaceContext } from '../../workspace/context'; +import { IssueMessageHighlighting } from '../IssueMessageHighlighting'; import IssueMessageTags from './IssueMessageTags'; export interface IssueMessageProps { @@ -29,20 +31,30 @@ export interface IssueMessageProps { quickFixAvailable?: boolean; displayWhyIsThisAnIssue?: boolean; message: string; + messageFormattings?: MessageFormatting[]; ruleKey: string; ruleStatus?: RuleStatus; } export default function IssueMessage(props: IssueMessageProps) { - const { engine, quickFixAvailable, message, ruleKey, ruleStatus, displayWhyIsThisAnIssue } = - props; + const { + engine, + quickFixAvailable, + message, + messageFormattings, + ruleKey, + ruleStatus, + displayWhyIsThisAnIssue, + } = props; const { openRule } = React.useContext(WorkspaceContext); return ( <>
- {message} + + + diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap index 3a8e73822e2..bc1d76c8876 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap @@ -8,7 +8,9 @@ exports[`should render correctly: default 1`] = ` - Reduce the number of conditional operators (4) used in the expression +
@@ -30,7 +32,9 @@ exports[`should render correctly: hide why is it an issue 1`] = ` - Reduce the number of conditional operators (4) used in the expression +
@@ -45,7 +49,9 @@ exports[`should render correctly: is deprecated rule 1`] = ` - Reduce the number of conditional operators (4) used in the expression + - Reduce the number of conditional operators (4) used in the expression + - Reduce the number of conditional operators (4) used in the expression + - Reduce the number of conditional operators (4) used in the expression + { + renderLocation = ( + index: number, + message: string | undefined, + messageFormattings: MessageFormatting[] | undefined + ) => { return ( @@ -132,18 +138,28 @@ export default class CrossFileLocationNavigator extends React.PureComponent {group.locations.length > 0 && (
- {onlyFirst && this.renderLocation(firstLocationIndex, group.locations[0].msg)} + {onlyFirst && + this.renderLocation( + firstLocationIndex, + group.locations[0].msg, + group.locations[0].msgFormattings + )} {onlyLast && this.renderLocation( firstLocationIndex + lastLocationIndex, - group.locations[lastLocationIndex].msg + group.locations[lastLocationIndex].msg, + group.locations[lastLocationIndex].msgFormattings )} {!onlyFirst && !onlyLast && group.locations.map((location, index) => - this.renderLocation(firstLocationIndex + index, location.msg) + this.renderLocation( + firstLocationIndex + index, + location.msg, + location.msgFormattings + ) )}
)} diff --git a/server/sonar-web/src/main/js/components/locations/FlowsList.tsx b/server/sonar-web/src/main/js/components/locations/FlowsList.tsx index 088ef497a2f..6c8532567c7 100644 --- a/server/sonar-web/src/main/js/components/locations/FlowsList.tsx +++ b/server/sonar-web/src/main/js/components/locations/FlowsList.tsx @@ -74,6 +74,7 @@ export default function FlowsList(props: Props) { diff --git a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx index 635095b65af..cdc62d64339 100644 --- a/server/sonar-web/src/main/js/components/locations/LocationsList.tsx +++ b/server/sonar-web/src/main/js/components/locations/LocationsList.tsx @@ -52,13 +52,14 @@ export default class LocationsList extends React.PureComponent { ); } return ( -
    +
      {locations.map((location, index) => ( // eslint-disable-next-line react/no-array-index-key
    • diff --git a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx index 5a7e2819f52..c4dcced8497 100644 --- a/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx +++ b/server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx @@ -19,14 +19,17 @@ */ import classNames from 'classnames'; import * as React from 'react'; +import { MessageFormatting } from '../../types/issues'; import LocationIndex from '../common/LocationIndex'; import LocationMessage from '../common/LocationMessage'; import { ButtonPlain } from '../controls/buttons'; +import { IssueMessageHighlighting } from '../issue/IssueMessageHighlighting'; import './SingleFileLocationNavigator.css'; interface Props { index: number; message: string | undefined; + messageFormattings?: MessageFormatting[]; onClick: (index: number) => void; selected: boolean; } @@ -59,7 +62,7 @@ export default class SingleFileLocationNavigator extends React.PureComponent {index + 1} - {message} + + {} + ); } diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap index 7e992bb77ff..8421b5c343e 100644 --- a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render locations in the same file 1`] = `
      • 1 - + + + `; @@ -28,6 +32,10 @@ exports[`should render correctly: index 2 1`] = ` 2 - + + + `; diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index e4b1bbf3312..bf885d57bd5 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { FlowLocation, Issue, Paging, TextRange } from './types'; +import { Issue, Paging, TextRange } from './types'; import { UserBase } from './users'; export enum IssueType { @@ -49,6 +49,24 @@ interface Comment { updatable: boolean; } +export interface MessageFormatting { + start: number; + end: number; + type: MessageFormattingType; +} + +export enum MessageFormattingType { + CODE = 'CODE', +} + +export interface RawFlowLocation { + component: string; + index?: number; + msg?: string; + msgFormattings?: MessageFormatting[]; + textRange: TextRange; +} + export interface RawIssue { actions: string[]; transitions?: string[]; @@ -60,10 +78,11 @@ export interface RawIssue { flows?: Array<{ type?: string; description?: string; - locations?: Array>; + locations?: RawFlowLocation[]; }>; key: string; line?: number; + messageFormattings?: MessageFormatting[]; project: string; rule: string; resolution?: string; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index e62a646955f..806046112e8 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -20,6 +20,7 @@ import { RuleDescriptionSection } from '../apps/coding-rules/rule'; import { ComponentQualifier } from './component'; +import { MessageFormatting } from './issues'; import { UserActive, UserBase } from './users'; export type Dict = { [key: string]: T }; @@ -206,6 +207,7 @@ export interface FlowLocation { componentName?: string; index?: number; msg?: string; + msgFormattings?: MessageFormatting[]; textRange: TextRange; } @@ -252,6 +254,7 @@ export interface Issue { flowsWithType: Flow[]; line?: number; message: string; + messageFormattings?: MessageFormatting[]; project: string; projectName: string; projectKey: string; @@ -324,6 +327,7 @@ export interface LinearIssueLocation { line: number; startLine?: number; text?: string; + textFormatting?: MessageFormatting[]; to: number; } -- cgit v1.2.3