diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-04-13 09:16:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-13 09:16:32 +0200 |
commit | 81266317348bddac9d237aee07141024d76db9b4 (patch) | |
tree | 5a81cc68aca22e5a6bada170764799bfb8f3f5c8 /server/sonar-web/src | |
parent | ee72d1678ac7d13fc093ebb90115eb807f7a2568 (diff) | |
download | sonarqube-81266317348bddac9d237aee07141024d76db9b4.tar.gz sonarqube-81266317348bddac9d237aee07141024d76db9b4.zip |
support multiple highlighted symbols (#1927)
Diffstat (limited to 'server/sonar-web/src')
9 files changed, 136 insertions, 38 deletions
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<string>, issues?: Array<Issue>, issuesByLine: { [number]: Array<string> }, issueLocationsByLine: { [number]: Array<LinearIssueLocation> }, @@ -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<string>) => { + 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<string>, issues: Array<Issue>, issuesByLine: { [number]: Array<string> }, issueLocationsByLine: { [number]: Array<LinearIssueLocation> }, @@ -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<string>) => 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<string>, issueLocations: Array<LinearIssueLocation>, issues: Array<string>, 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<string>) => void, openIssues: boolean, previousLine?: SourceLine, selectedIssue: string | null, @@ -136,7 +136,7 @@ export default class Line extends React.PureComponent { </td>} <LineCode - highlightedSymbol={this.props.highlightedSymbol} + highlightedSymbols={this.props.highlightedSymbols} issueKeys={this.props.issues} issueLocations={this.props.issueLocations} line={line} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js index d8da31965d3..b5813f76365 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js @@ -36,13 +36,13 @@ import type { } from '../helpers/indexing'; type Props = { - highlightedSymbol: string | null, + highlightedSymbols: Array<string>, issueKeys: Array<string>, issueLocations: Array<LinearIssueLocation>, line: SourceLine, onIssueSelect: (issueKey: string) => void, onLocationSelect: (flowIndex: number, locationIndex: number) => void, - onSymbolClick: (symbol: string) => void, + onSymbolClick: (Array<string>) => void, // $FlowFixMe secondaryIssueLocations: Array<IndexedIssueLocation>, secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>, @@ -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( <LineCode - highlightedSymbol="sym1" + highlightedSymbols={['sym1']} issueKeys={['issue-1', 'issue-2']} issueLocations={issueLocations} line={line} @@ -61,7 +61,7 @@ it('should handle empty location message', () => { const selectedIssueLocation = { from: 6, to: 9, line: 3, flowIndex: 0, locationIndex: 0 }; const wrapper = shallow( <LineCode - highlightedSymbol="sym1" + highlightedSymbols={['sym1']} issueKeys={['issue-1', 'issue-2']} issueLocations={issueLocations} line={line} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js new file mode 100644 index 00000000000..9a7e5439277 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js @@ -0,0 +1,56 @@ +/* + * 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. + */ +// @flow +import { highlightSymbol } from '../highlight'; + +describe('highlightSymbol', () => { + 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: '<span class="sym-54 sym"><span class="sym-56 sym">foo</span></span>' }, + { line: 2, code: '<span class="sym-56 sym">bar</span>' }, + { line: 3, code: '<span class="k">qux</span>' } + ]; + 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<SourceLine>) => { 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; }; |