From 81266317348bddac9d237aee07141024d76db9b4 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 13 Apr 2017 09:16:32 +0200 Subject: [PATCH] support multiple highlighted symbols (#1927) --- .../SourceViewer/SourceViewerBase.js | 18 +++--- .../SourceViewer/SourceViewerCode.js | 17 +++--- .../SourceViewer/components/Line.js | 6 +- .../SourceViewer/components/LineCode.js | 18 +++--- .../components/__tests__/LineCode-test.js | 4 +- .../helpers/__tests__/highlight-test.js | 56 +++++++++++++++++++ .../helpers/__tests__/indexing-test.js | 35 ++++++++++++ .../SourceViewer/helpers/highlight.js | 8 ++- .../SourceViewer/helpers/indexing.js | 12 ++-- 9 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index ce5ba82b3bb..c0cae1208f8 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import classNames from 'classnames'; -import { uniqBy } from 'lodash'; +import { intersection, uniqBy } from 'lodash'; import SourceViewerHeader from './SourceViewerHeader'; import SourceViewerCode from './SourceViewerCode'; import SourceViewerIssueLocations from './SourceViewerIssueLocations'; @@ -91,7 +91,7 @@ type State = { duplicatedFiles?: Array<{ key: string }>, hasSourcesAfter: boolean, highlightedLine: number | null, - highlightedSymbol: string | null, + highlightedSymbols: Array, issues?: Array, issuesByLine: { [number]: Array }, issueLocationsByLine: { [number]: Array }, @@ -143,7 +143,7 @@ export default class SourceViewerBase extends React.Component { duplicationsByLine: {}, hasSourcesAfter: false, highlightedLine: props.highlightedLine || null, - highlightedSymbol: null, + highlightedSymbols: [], issuesByLine: {}, issueLocationsByLine: {}, issueSecondaryLocationsByIssueByLine: {}, @@ -477,10 +477,12 @@ export default class SourceViewerBase extends React.Component { this.displayLinePopup(line.line, element); }; - handleSymbolClick = (symbol: string) => { - this.setState(prevState => ({ - highlightedSymbol: prevState.highlightedSymbol === symbol ? null : symbol - })); + handleSymbolClick = (symbols: Array) => { + this.setState(state => { + const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0; + const highlightedSymbols = shouldDisable ? [] : symbols; + return { highlightedSymbols }; + }); }; handleSCMClick = (line: SourceLine, element: HTMLElement) => { @@ -544,7 +546,7 @@ export default class SourceViewerBase extends React.Component { hasSourcesAfter={this.state.hasSourcesAfter} filterLine={this.props.filterLine} highlightedLine={this.state.highlightedLine} - highlightedSymbol={this.state.highlightedSymbol} + highlightedSymbols={this.state.highlightedSymbols} issues={this.state.issues} issuesByLine={this.state.issuesByLine} issueLocationsByLine={this.state.issueLocationsByLine} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js index be6d1a27173..8b9cfb46bd5 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import { intersection } from 'lodash'; import Line from './components/Line'; import { translate } from '../../helpers/l10n'; import type { Duplication, SourceLine } from './types'; @@ -48,7 +49,7 @@ export default class SourceViewerCode extends React.PureComponent { hasSourcesAfter: boolean, hasSourcesBefore: boolean, highlightedLine: number | null, - highlightedSymbol: string | null, + highlightedSymbols: Array, issues: Array, issuesByLine: { [number]: Array }, issueLocationsByLine: { [number]: Array }, @@ -68,7 +69,7 @@ export default class SourceViewerCode extends React.PureComponent { onLineClick: (SourceLine, HTMLElement) => void, onSCMClick: (SourceLine, HTMLElement) => void, onLocationSelect: (flowIndex: number, locationIndex: number) => void, - onSymbolClick: (string) => void, + onSymbolClick: (Array) => void, openIssuesByLine: { [number]: boolean }, selectedIssue: string | null, selectedIssueLocation: IndexedIssueLocation | null, @@ -124,11 +125,11 @@ export default class SourceViewerCode extends React.PureComponent { // for the following properties pass null if the line for sure is not impacted const symbolsForLine = this.props.symbolsByLine[line.line] || []; - const { highlightedSymbol } = this.props; - const optimizedHighlightedSymbol = highlightedSymbol != null && - symbolsForLine.includes(highlightedSymbol) - ? highlightedSymbol - : null; + const { highlightedSymbols } = this.props; + let optimizedHighlightedSymbols = intersection(symbolsForLine, highlightedSymbols); + if (!optimizedHighlightedSymbols.length) { + optimizedHighlightedSymbols = EMPTY_ARRAY; + } const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) ? selectedIssue @@ -155,7 +156,7 @@ export default class SourceViewerCode extends React.PureComponent { duplicationsCount={duplicationsCount} filtered={filtered} highlighted={line.line === this.props.highlightedLine} - highlightedSymbol={optimizedHighlightedSymbol} + highlightedSymbols={optimizedHighlightedSymbols} issueLocations={this.getIssueLocationsForLine(line)} issues={issuesForLine} key={line.line} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js index bc5e80c2e1f..74f3dabbfae 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js @@ -46,7 +46,7 @@ type Props = { duplicationsCount: number, filtered: boolean | null, highlighted: boolean, - highlightedSymbol: string | null, + highlightedSymbols: Array, issueLocations: Array, issues: Array, line: SourceLine, @@ -60,7 +60,7 @@ type Props = { onIssuesClose: (SourceLine) => void, onSCMClick: (SourceLine, HTMLElement) => void, onLocationSelect: (flowIndex: number, locationIndex: number) => void, - onSymbolClick: (string) => void, + onSymbolClick: (Array) => void, openIssues: boolean, previousLine?: SourceLine, selectedIssue: string | null, @@ -136,7 +136,7 @@ export default class Line extends React.PureComponent { } , issueKeys: Array, issueLocations: Array, line: SourceLine, onIssueSelect: (issueKey: string) => void, onLocationSelect: (flowIndex: number, locationIndex: number) => void, - onSymbolClick: (symbol: string) => void, + onSymbolClick: (Array) => void, // $FlowFixMe secondaryIssueLocations: Array, secondaryIssueLocationMessages: Array, @@ -109,9 +109,9 @@ export default class LineCode extends React.PureComponent { handleSymbolClick = (e: Object) => { e.preventDefault(); - const key = e.currentTarget.className.match(/sym-\d+/); - if (key && key[0]) { - this.props.onSymbolClick(key[0]); + const keys = e.currentTarget.className.match(/sym-\d+/g); + if (keys.length > 0) { + this.props.onSymbolClick(keys); } }; @@ -165,7 +165,7 @@ export default class LineCode extends React.PureComponent { render() { const { - highlightedSymbol, + highlightedSymbols, issueKeys, issueLocations, line, @@ -179,9 +179,9 @@ export default class LineCode extends React.PureComponent { let tokens = [...this.state.tokens]; - if (highlightedSymbol) { - tokens = highlightSymbol(tokens, highlightedSymbol); - } + highlightedSymbols.forEach(symbol => { + tokens = highlightSymbol(tokens, symbol); + }); if (issueLocations.length > 0) { tokens = highlightIssueLocations(tokens, issueLocations); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js index fa78dfa5afb..3cc6793b214 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js @@ -33,7 +33,7 @@ it('render code', () => { const selectedIssueLocation = { from: 6, to: 9, line: 3, flowIndex: 0, locationIndex: 0 }; const wrapper = shallow( { const selectedIssueLocation = { from: 6, to: 9, line: 3, flowIndex: 0, locationIndex: 0 }; const wrapper = shallow( { + it('should not highlight symbols with similar beginning', () => { + // test all positions of sym-X in the string: beginning, middle and ending + const tokens = [ + { className: 'sym-18 b', text: 'foo' }, + { className: 'a sym-18', text: 'foo' }, + { className: 'a sym-18 b', text: 'foo' }, + { className: 'sym-1 d', text: 'bar' }, + { className: 'c sym-1', text: 'bar' }, + { className: 'c sym-1 d', text: 'bar' } + ]; + expect(highlightSymbol(tokens, 'sym-1')).toEqual([ + { className: 'sym-18 b', text: 'foo' }, + { className: 'a sym-18', text: 'foo' }, + { className: 'a sym-18 b', text: 'foo' }, + { className: 'sym-1 d highlighted', text: 'bar' }, + { className: 'c sym-1 highlighted', text: 'bar' }, + { className: 'c sym-1 d highlighted', text: 'bar' } + ]); + }); + + it('should highlight symbols marked twice', () => { + const tokens = [ + { className: 'sym sym-1 sym sym-2', text: 'foo' }, + { className: 'sym sym-1', text: 'bar' }, + { className: 'sym sym-2', text: 'qux' } + ]; + expect(highlightSymbol(tokens, 'sym-1')).toEqual([ + { className: 'sym sym-1 sym sym-2 highlighted', text: 'foo' }, + { className: 'sym sym-1 highlighted', text: 'bar' }, + { className: 'sym sym-2', text: 'qux' } + ]); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js new file mode 100644 index 00000000000..ef5f66e2d72 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { symbolsByLine } from '../indexing'; + +describe('symbolsByLine', () => { + it('should highlight symbols marked twice', () => { + const lines = [ + { line: 1, code: 'foo' }, + { line: 2, code: 'bar' }, + { line: 3, code: 'qux' } + ]; + expect(symbolsByLine(lines)).toEqual({ + 1: ['sym-54', 'sym-56'], + 2: ['sym-56'], + 3: [] + }); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js index c742f2b0d4c..8bba53b8755 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js @@ -45,13 +45,15 @@ export const splitByTokens = (code: string, rootClassName: string = ''): Tokens return tokens; }; -export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => - tokens.map( +export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => { + const symbolRegExp = new RegExp(`\\b${symbol}\\b`); + return tokens.map( token => - token.className.includes(symbol) + symbolRegExp.test(token.className) ? { ...token, className: `${token.className} highlighted` } : token ); +}; /** * Intersect two ranges diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js index b1c79ac0f11..36bf7e73b3a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow +import { flatten } from 'lodash'; import { splitByTokens } from './highlight'; import { getLinearLocations, getIssueLocations } from './issueLocations'; import type { Issue } from '../../issue/types'; @@ -149,12 +150,13 @@ export const symbolsByLine = (sources: Array) => { const index = {}; sources.forEach(line => { const tokens = splitByTokens(line.code); - index[line.line] = tokens - .map(token => { - const key = token.className.match(/sym-\d+/); - return key && key[0]; + const symbols = flatten( + tokens.map(token => { + const keys = token.className.match(/sym-\d+/g); + return keys != null ? keys : []; }) - .filter(key => key); + ); + index[line.line] = symbols.filter(key => key); }); return index; }; -- 2.39.5