aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Cho-Lerat <david.lerat@sonarsource.com>2022-11-29 17:26:51 +0100
committersonartech <sonartech@sonarsource.com>2022-12-01 20:03:12 +0000
commitfd9fe4dba0dbb7c359613131e64e84110ea75a09 (patch)
tree539a99ec8cab0a0b8a3e831d2116f676712be777
parent6c42508ad46f28bd0e3c29a245fcd095a9714e6f (diff)
downloadsonarqube-fd9fe4dba0dbb7c359613131e64e84110ea75a09.tar.gz
sonarqube-fd9fe4dba0dbb7c359613131e64e84110ea75a09.zip
SONAR-17592 Code highlighting for issue messages and code locations
-rw-r--r--server/sonar-web/src/main/js/app/theme.js3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueBox-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css12
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx25
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts1
-rw-r--r--server/sonar-web/src/main/js/components/common/LocationMessage.css3
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tooltip.css4
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.css9
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueMessageHighlighting.tsx102
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/IssueMessageHighlighting-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueMessageHighlighting-test.tsx.snap100
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx24
-rw-r--r--server/sonar-web/src/main/js/components/locations/FlowsList.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/locations/LocationsList.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/locations/SingleFileLocationNavigator.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/LocationsList-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/types/issues.ts23
-rw-r--r--server/sonar-web/src/main/js/types/types.ts4
26 files changed, 453 insertions, 35 deletions
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<Props, State> {
<>
<div className="display-flex-center display-flex-space-between big-padded-top">
<h1 className="text-bold spacer-right">
- <span className="spacer-right">{issue.message}</span>
+ <span className="spacer-right issue-header" aria-label={issue.message}>
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
+ </span>
<IssueMessageTags
engine={issue.externalRuleEngine}
quickFixAvailable={quickFixAvailable}
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
index 59504f3543b..5ff855693b6 100644
--- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
@@ -20,6 +20,7 @@
import classNames from 'classnames';
import * as React from 'react';
import { ButtonPlain } from '../../../components/controls/buttons';
+import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
import FlowsList from '../../../components/locations/FlowsList';
import LocationsList from '../../../components/locations/LocationsList';
import TypeHelper from '../../../components/shared/TypeHelper';
@@ -82,7 +83,10 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
innerRef={(node) => (this.messageElement = node)}
onClick={this.handleClick}
>
- {issue.message}
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
</ButtonPlain>
<div className="concise-issue-box-attributes">
<TypeHelper className="display-block little-spacer-right" type={issue.type} />
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
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</ButtonPlain>
<div
className="concise-issue-box-attributes"
@@ -70,7 +72,9 @@ exports[`should render correctly 2`] = `
innerRef={[Function]}
onClick={[Function]}
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</ButtonPlain>
<div
className="concise-issue-box-attributes"
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index b544224e116..d5d1019dec5 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -68,6 +68,10 @@
transition: background-color 0.3s ease, border-color 0.3s ease;
}
+.concise-issue-box .issue-message-highlight-CODE {
+ background-color: var(--blacka06);
+}
+
.concise-issue-box:hover {
border: 2px dashed var(--blue);
}
@@ -87,8 +91,7 @@
.concise-issue-box-message:hover,
.concise-issue-box-message:active {
display: block;
- overflow: hidden;
- text-overflow: ellipsis;
+ word-break: break-word;
font-weight: bold !important;
color: inherit !important;
text-align: left;
@@ -175,6 +178,11 @@
}
}
+.issue-header .issue-message-highlight-CODE {
+ background-color: var(--blacka06);
+ border-radius: 5px;
+}
+
.issue-location {
display: inline-block;
vertical-align: top;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
index e14ce3f6e4f..61415ca2f34 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
@@ -20,9 +20,11 @@
import classNames from 'classnames';
import * as React from 'react';
import { IssueSourceViewerScrollContext } from '../../../apps/issues/components/IssueSourceViewerScrollContext';
+import { MessageFormatting } from '../../../types/issues';
import { LinearIssueLocation, SourceLine } from '../../../types/types';
import LocationIndex from '../../common/LocationIndex';
import Tooltip from '../../controls/Tooltip';
+import { IssueMessageHighlighting } from '../../issue/IssueMessageHighlighting';
import {
highlightIssueLocations,
highlightSymbol,
@@ -95,8 +97,11 @@ export default class LineCode extends React.PureComponent<React.PropsWithChildre
const selected =
highlightedLocationMessage !== undefined && highlightedLocationMessage.index === marker;
const loc = secondaryIssueLocations.find((loc) => 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<React.PropsWithChildre
return renderedTokens;
}
- renderMarker(index: number, message: string | undefined, selected: boolean, leading: boolean) {
+ renderMarker(
+ index: number,
+ message: string | undefined,
+ messageFormattings: MessageFormatting[] | undefined,
+ selected: boolean,
+ leading: boolean
+ ) {
const { onLocationSelect } = this.props;
const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
return (
- <Tooltip key={`marker-${index}`} overlay={message} placement="top">
+ <Tooltip
+ key={`marker-${index}`}
+ overlay={
+ <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+ }
+ placement="top"
+ >
<LocationIndex
leading={leading}
onClick={onClick}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
index 308cb3b368f..24aff0fb75d 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
@@ -108,7 +108,11 @@ exports[`render code: with secondary location 1`] = `
</span>
<Tooltip
key="marker-1"
- overlay="secondary-location-msg"
+ overlay={
+ <IssueMessageHighlighting
+ message="secondary-location-msg"
+ />
+ }
placement="top"
>
<LocationIndex
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts
index 241e36ac4b2..8a39a3db3d1 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.ts
@@ -51,6 +51,7 @@ export function getSecondaryIssueLocationsForLine(
startLine: location.textRange.startLine,
index: location.index,
text: location.msg,
+ textFormatting: location.msgFormattings,
}))
: [];
return [...locations, ...linearLocations];
diff --git a/server/sonar-web/src/main/js/components/common/LocationMessage.css b/server/sonar-web/src/main/js/components/common/LocationMessage.css
index a9269cd242a..f72b16629a8 100644
--- a/server/sonar-web/src/main/js/components/common/LocationMessage.css
+++ b/server/sonar-web/src/main/js/components/common/LocationMessage.css
@@ -23,8 +23,7 @@
line-height: 16px;
padding: 0 6px;
font-size: var(--smallFontSize);
- text-overflow: ellipsis;
- overflow: hidden;
+ word-break: break-word;
}
.location-index + .location-message {
diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.css b/server/sonar-web/src/main/js/components/controls/Tooltip.css
index cd9696a00a2..fc00a8e5d29 100644
--- a/server/sonar-web/src/main/js/components/controls/Tooltip.css
+++ b/server/sonar-web/src/main/js/components/controls/Tooltip.css
@@ -120,6 +120,10 @@
pointer-events: none;
}
+.tooltip .issue-message-highlight-CODE {
+ background-color: var(--whitea18);
+}
+
@keyframes fadeIn {
from {
opacity: 0;
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css
index 397d8781c14..d42221c1de7 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.css
+++ b/server/sonar-web/src/main/js/components/issue/Issue.css
@@ -262,6 +262,15 @@
margin: 10px 0px;
}
+.issue-message-highlight-CODE {
+ background-color: var(--whitea60);
+ border-radius: 4px;
+ font-family: var(--sourceCodeFontFamily);
+ font-weight: 400;
+ line-height: 1.4em;
+ padding: 2px 2px 0;
+}
+
.issue-message-box.secondary-issue {
background-color: var(--secondIssueBgColor);
}
diff --git a/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx b/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
index 2abe1d8da8e..bad13effc47 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
+++ b/server/sonar-web/src/main/js/components/issue/IssueMessageBox.tsx
@@ -23,6 +23,7 @@ import { colors } from '../../app/theme';
import { Issue } from '../../types/types';
import IssueTypeIcon from '../icons/IssueTypeIcon';
import './Issue.css';
+import { IssueMessageHighlighting } from './IssueMessageHighlighting';
export interface IssueMessageBoxProps {
selected: boolean;
@@ -50,7 +51,10 @@ export function IssueMessageBox(props: IssueMessageBoxProps, ref: React.Forwarde
fill={colors.baseFontColor}
query={issue.type}
/>
- {issue.message}
+ <IssueMessageHighlighting
+ message={issue.message}
+ messageFormattings={issue.messageFormattings}
+ />
</div>
);
}
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 (
+ <span>
+ {sanitizedFormattings.map(({ start, end, type }) => {
+ const beginning = previousEnd;
+ previousEnd = end;
+
+ return (
+ <React.Fragment key={`${message}-${start}-${end}`}>
+ {message.slice(beginning, start)}
+ <span
+ className={classNames({
+ 'issue-message-highlight-CODE': type === MessageFormattingType.CODE,
+ })}
+ >
+ {message.slice(start, end)}
+ </span>
+ </React.Fragment>
+ );
+ })}
+
+ {message.slice(previousEnd)}
+ </span>
+ );
+}
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<IssueMessageHighlightingProps> = {}) {
+ return renderComponent(<IssueMessageHighlighting {...props} />);
+}
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`] = `<DocumentFragment />`;
+
+exports[`should format the string with highlights 2`] = `
+<DocumentFragment>
+ message
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 3`] = `
+<DocumentFragment>
+ message
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 4`] = `
+<DocumentFragment>
+ <span>
+ message
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 5`] = `
+<DocumentFragment>
+ <span>
+ m
+ <span
+ class="issue-message-highlight-CODE"
+ >
+ ess
+ </span>
+ a
+ <span
+ class="issue-message-highlight-CODE"
+ >
+ g
+ </span>
+ e
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 6`] = `
+<DocumentFragment>
+ <span>
+ a somewhat longer message with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 7`] = `
+<DocumentFragment>
+ <span>
+ a somewhat longer message with overlapping range
+ <span
+ class="issue-message-highlight-CODE"
+ >
+ s
+ </span>
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 8`] = `
+<DocumentFragment>
+ <span>
+ a somewhat longer message with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 9`] = `
+<DocumentFragment>
+ <span>
+ a
+ <span
+ class="issue-message-highlight-CODE"
+ >
+ somewhat longer message
+ </span>
+ with overlapping ranges
+ </span>
+</DocumentFragment>
+`;
+
+exports[`should format the string with highlights 10`] = `
+<DocumentFragment>
+ <span>
+ a
+ <span
+ class="issue-message-highlight-CODE"
+ >
+ somewhat longer message with
+ </span>
+ overlapping ranges
+ </span>
+</DocumentFragment>
+`;
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 (
<>
<div className="display-inline-flex-center issue-message break-word">
- <span className="spacer-right">{message}</span>
+ <span className="spacer-right">
+ <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
+ </span>
<IssueMessageTags
engine={engine}
quickFixAvailable={quickFixAvailable}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx
index 3fb1ec8dae3..bb90389607c 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx
@@ -80,6 +80,7 @@ export default function IssueTitleBar(props: IssueTitleBarProps) {
quickFixAvailable={issue.quickFixAvailable}
displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
message={issue.message}
+ messageFormattings={issue.messageFormattings}
ruleKey={issue.rule}
ruleStatus={issue.ruleStatus as RuleStatus | undefined}
/>
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`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags />
</div>
@@ -30,7 +32,9 @@ exports[`should render correctly: hide why is it an issue 1`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags />
</div>
@@ -45,7 +49,9 @@ exports[`should render correctly: is deprecated rule 1`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags
ruleStatus="DEPRECATED"
@@ -69,7 +75,9 @@ exports[`should render correctly: is removed rule 1`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags
ruleStatus="REMOVED"
@@ -93,7 +101,9 @@ exports[`should render correctly: with engine info 1`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags
engine="js"
@@ -117,7 +127,9 @@ exports[`should render correctly: with quick fix 1`] = `
<span
className="spacer-right"
>
- Reduce the number of conditional operators (4) used in the expression
+ <IssueMessageHighlighting
+ message="Reduce the number of conditional operators (4) used in the expression"
+ />
</span>
<IssueMessageTags
quickFixAvailable={true}
diff --git a/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx
index cb6581f1b3b..da04e78f600 100644
--- a/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx
+++ b/server/sonar-web/src/main/js/components/locations/CrossFileLocationNavigator.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { translateWithParameters } from '../../helpers/l10n';
import { collapsePath } from '../../helpers/path';
+import { MessageFormatting } from '../../types/issues';
import { FlowLocation } from '../../types/types';
import './CrossFileLocationNavigator.css';
import SingleFileLocationNavigator from './SingleFileLocationNavigator';
@@ -105,12 +106,17 @@ export default class CrossFileLocationNavigator extends React.PureComponent<Prop
return groups;
};
- renderLocation = (index: number, message: string | undefined) => {
+ renderLocation = (
+ index: number,
+ message: string | undefined,
+ messageFormattings: MessageFormatting[] | undefined
+ ) => {
return (
<SingleFileLocationNavigator
index={index}
key={index}
message={message}
+ messageFormattings={messageFormattings}
onClick={this.props.onLocationSelect}
selected={index === this.props.selectedLocationIndex}
/>
@@ -132,18 +138,28 @@ export default class CrossFileLocationNavigator extends React.PureComponent<Prop
</div>
{group.locations.length > 0 && (
<div className="location-file-locations">
- {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
+ )
)}
</div>
)}
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) {
<SingleFileLocationNavigator
index={locIndex}
message={location.msg}
+ messageFormattings={location.msgFormattings}
onClick={props.onLocationSelect}
selected={locIndex === selectedLocationIndex}
/>
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<Props> {
);
}
return (
- <ul className="spacer-top ">
+ <ul className="spacer-top">
{locations.map((location, index) => (
// eslint-disable-next-line react/no-array-index-key
<li className="display-flex-column" key={index}>
<SingleFileLocationNavigator
index={index}
message={location.msg}
+ messageFormattings={location.msgFormattings}
onClick={this.props.onLocationSelect}
selected={index === selectedLocationIndex}
/>
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<Pro
};
render() {
- const { index, message, selected } = this.props;
+ const { index, message, messageFormattings, selected } = this.props;
return (
<ButtonPlain
@@ -73,7 +76,9 @@ export default class SingleFileLocationNavigator extends React.PureComponent<Pro
onClick={this.handleClick}
>
<LocationIndex>{index + 1}</LocationIndex>
- <LocationMessage>{message}</LocationMessage>
+ <LocationMessage>
+ {<IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />}
+ </LocationMessage>
</ButtonPlain>
);
}
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`] = `
<ul
- className="spacer-top "
+ className="spacer-top"
>
<li
className="display-flex-column"
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap
index ec2618e04cc..3231f68233d 100644
--- a/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/locations/__tests__/__snapshots__/SingleFileLocationsNavigator-test.tsx.snap
@@ -12,7 +12,11 @@ exports[`should render correctly: index 1 1`] = `
<LocationIndex>
1
</LocationIndex>
- <LocationMessage />
+ <LocationMessage>
+ <IssueMessageHighlighting
+ message=""
+ />
+ </LocationMessage>
</ButtonPlain>
`;
@@ -28,6 +32,10 @@ exports[`should render correctly: index 2 1`] = `
<LocationIndex>
2
</LocationIndex>
- <LocationMessage />
+ <LocationMessage>
+ <IssueMessageHighlighting
+ message=""
+ />
+ </LocationMessage>
</ButtonPlain>
`;
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<Omit<FlowLocation, 'componentName'>>;
+ 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<T> = { [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;
}