aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js')
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js377
1 files changed, 377 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
new file mode 100644
index 00000000000..72cb0d5c053
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js
@@ -0,0 +1,377 @@
+/*
+ * 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 React from 'react';
+import classNames from 'classnames';
+import times from 'lodash/times';
+import ConnectedIssue from '../issue/ConnectedIssue';
+import SourceViewerIssuesIndicator from './SourceViewerIssuesIndicator';
+import { translate } from '../../helpers/l10n';
+import { splitByTokens, highlightSymbol, highlightIssueLocations, generateHTML } from './helpers/highlight';
+import type { SourceLine } from './types';
+
+type Props = {
+ displayAllIssues: boolean,
+ displayCoverage: boolean,
+ displayDuplications: boolean,
+ displayFiltered: boolean,
+ displayIssues: boolean,
+ displaySCM: boolean,
+ duplications: Array<number>,
+ duplicationsCount: number,
+ filtered: boolean | null,
+ highlighted: boolean,
+ highlightedSymbol: string | null,
+ issueLocations: Array<{ from: number, to: number }>,
+ issues: Array<string>,
+ line: SourceLine,
+ loadDuplications: (SourceLine, HTMLElement) => void,
+ onClick: (number, HTMLElement) => void,
+ onCoverageClick: (SourceLine, HTMLElement) => void,
+ onDuplicationClick: (number, number) => void,
+ onIssueSelect: (string) => void,
+ onIssueUnselect: () => void,
+ onSCMClick: (SourceLine, HTMLElement) => void,
+ onSymbolClick: (string) => void,
+ selectedIssue: string | null,
+ // $FlowFixMe
+ secondaryIssueLocations: Array<{ from: number, to: number }>,
+ // $FlowFixMe
+ secondaryIssueLocationMessages: Array<{ msg: string, index?: number }>
+};
+
+type State = {
+ issuesOpen: boolean
+};
+
+export default class SourceViewerLine extends React.PureComponent {
+ codeNode: HTMLElement;
+ props: Props;
+ issueElements: { [string]: HTMLElement } = {};
+ issueViews: { [string]: { destroy: () => void } } = {};
+ state: State = { issuesOpen: false };
+ symbols: NodeList<HTMLElement>;
+
+ componentDidMount () {
+ this.attachEvents();
+ }
+
+ componentWillUpdate () {
+ this.detachEvents();
+ }
+
+ componentDidUpdate (prevProps: Props) {
+ /* eslint-disable no-console */
+ console.log('re-render line', this.props.line.line, 'because they are not equal:');
+ Object.keys(this.props).forEach(prop => {
+ if (this.props[prop] !== prevProps[prop]) {
+ console.log(prop);
+ }
+ });
+ console.log('');
+
+ this.attachEvents();
+ }
+
+ componentWillUnmount () {
+ this.detachEvents();
+ }
+
+ attachEvents () {
+ this.symbols = this.codeNode.querySelectorAll('.sym');
+ for (const symbol of this.symbols) {
+ symbol.addEventListener('click', this.handleSymbolClick);
+ }
+ }
+
+ detachEvents () {
+ if (this.symbols) {
+ for (const symbol of this.symbols) {
+ symbol.removeEventListener('click', this.handleSymbolClick);
+ }
+ }
+ }
+
+ handleClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.props.onClick(this.props.line.line, e.target);
+ };
+
+ handleCoverageClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.props.onCoverageClick(this.props.line, e.target);
+ };
+
+ handleIssuesIndicatorClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.setState(prevState => {
+ // TODO not sure if side effects allowed here
+ if (!prevState.issuesOpen) {
+ const { issues } = this.props;
+ if (issues.length > 0) {
+ this.props.onIssueSelect(issues[0]);
+ }
+ } else {
+ this.props.onIssueUnselect();
+ }
+
+ return { issuesOpen: !prevState.issuesOpen };
+ });
+ }
+
+ handleSCMClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.props.onSCMClick(this.props.line, e.target);
+ }
+
+ handleSymbolClick = (e: Object) => {
+ e.preventDefault();
+ const key = e.currentTarget.className.match(/sym-\d+/);
+ if (key && key[0]) {
+ this.props.onSymbolClick(key[0]);
+ }
+ };
+
+ handleIssueSelect = (issueKey: string) => {
+ this.props.onIssueSelect(issueKey);
+ };
+
+ renderLineNumber () {
+ const { line } = this.props;
+ return (
+ <td className="source-meta source-line-number"
+ // don't display 0
+ data-line-number={line.line ? line.line : undefined}
+ role={line.line ? 'button' : undefined}
+ tabIndex={line.line ? 0 : undefined}
+ onClick={line.line ? this.handleClick : undefined}/>
+ );
+ }
+
+ renderSCM () {
+ const { line } = this.props;
+ const clickable = !!line.line;
+ return (
+ <td className="source-meta source-line-scm"
+ data-line-number={line.line}
+ role={clickable ? 'button' : undefined}
+ tabIndex={clickable ? 0 : undefined}
+ onClick={clickable ? this.handleSCMClick : undefined}>
+ {this.props.displaySCM && (
+ <div className="source-line-scm-inner" data-author={line.scmAuthor}/>
+ )}
+ </td>
+ );
+ }
+
+ renderCoverage () {
+ const { line } = this.props;
+ const className = 'source-meta source-line-coverage' +
+ (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
+ return (
+ <td className={className}
+ data-line-number={line.line}
+ title={line.coverageStatus != null && translate('source_viewer.tooltip', line.coverageStatus)}
+ data-placement={line.coverageStatus != null && 'right'}
+ data-toggle={line.coverageStatus != null && 'tooltip'}
+ role={line.coverageStatus != null ? 'button' : undefined}
+ tabIndex={line.coverageStatus != null ? 0 : undefined}
+ onClick={line.coverageStatus != null && this.handleCoverageClick}>
+ <div className="source-line-bar"/>
+ </td>
+ );
+ }
+
+ renderDuplications () {
+ const { line } = this.props;
+ const className = classNames('source-meta', 'source-line-duplications', {
+ 'source-line-duplicated': line.duplicated
+ });
+
+ const handleDuplicationClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.props.loadDuplications(this.props.line, e.target);
+ };
+
+ return (
+ <td className={className}
+ title={line.duplicated && translate('source_viewer.tooltip.duplicated_line')}
+ data-placement={line.duplicated && 'right'}
+ data-toggle={line.duplicated && 'tooltip'}
+ role="button"
+ tabIndex="0"
+ onClick={handleDuplicationClick}>
+ <div className="source-line-bar"/>
+ </td>
+ );
+ }
+
+ renderDuplicationsExtra () {
+ const { duplications, duplicationsCount } = this.props;
+ return times(duplicationsCount).map(index => this.renderDuplication(index, duplications.includes(index)));
+ }
+
+ renderDuplication = (index: number, duplicated: boolean) => {
+ const className = classNames('source-meta', 'source-line-duplications-extra', {
+ 'source-line-duplicated': duplicated
+ });
+
+ const handleDuplicationClick = (e: SyntheticInputEvent) => {
+ e.preventDefault();
+ this.props.onDuplicationClick(index, this.props.line.line);
+ };
+
+ return (
+ <td key={index}
+ className={className}
+ data-line-number={this.props.line.line}
+ data-index={index}
+ title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined}
+ data-placement={duplicated ? 'right' : undefined}
+ data-toggle={duplicated ? 'tooltip' : undefined}
+ role={duplicated ? 'button' : undefined}
+ tabIndex={duplicated ? '0' : undefined}
+ onClick={duplicated ? handleDuplicationClick : undefined}>
+ <div className="source-line-bar"/>
+ </td>
+ );
+ };
+
+ renderIssuesIndicator () {
+ const { issues } = this.props;
+ const hasIssues = issues.length > 0;
+ const className = classNames('source-meta', 'source-line-issues', { 'source-line-with-issues': hasIssues });
+ const onClick = hasIssues ? this.handleIssuesIndicatorClick : undefined;
+
+ return (
+ <td className={className}
+ data-line-number={this.props.line.line}
+ role="button"
+ tabIndex="0"
+ onClick={onClick}>
+ {hasIssues && (
+ <SourceViewerIssuesIndicator issues={issues}/>
+ )}
+ {issues.length > 1 && (
+ <span className="source-line-issues-counter">{issues.length}</span>
+ )}
+ </td>
+ );
+ }
+
+ renderSecondaryIssueLocationMessages (locationMessages: Array<{ msg: string, index?: number }>) {
+ const limitString = (str: string) => (
+ str.length > 30 ? str.substr(0, 30) + '...' : str
+ );
+
+ return (
+ <div className="source-line-issue-locations">
+ {locationMessages.map((locationMessage, index) => (
+ <div key={index} className="source-viewer-issue-location" title={locationMessage.msg}>
+ {locationMessage.index && (
+ <strong>{locationMessage.index}: </strong>
+ )}
+ {limitString(locationMessage.msg)}
+ </div>
+ ))}
+ </div>
+ );
+ }
+
+ renderCode () {
+ const { line, highlightedSymbol, issueLocations, issues, secondaryIssueLocations } = this.props;
+ const { secondaryIssueLocationMessages } = this.props;
+ const className = classNames('source-line-code', 'code', { 'has-issues': issues.length > 0 });
+
+ const code = line.code || '';
+ let tokens = splitByTokens(code);
+
+ if (highlightedSymbol) {
+ tokens = highlightSymbol(tokens, highlightedSymbol);
+ }
+
+ if (issueLocations.length > 0) {
+ tokens = highlightIssueLocations(tokens, issueLocations);
+ }
+
+ if (secondaryIssueLocations) {
+ tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'source-line-code-secondary-issue');
+ }
+
+ const finalCode = generateHTML(tokens);
+
+ const showIssues = (this.state.issuesOpen || this.props.displayAllIssues) && issues.length > 0;
+
+ return (
+ <td className={className} data-line-number={line.line}>
+ <div className="source-line-code-inner">
+ <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/>
+ {secondaryIssueLocationMessages != null && secondaryIssueLocationMessages.length > 0 && (
+ this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)
+ )}
+ </div>
+ {showIssues && (
+ <div className="issue-list">
+ {issues.map(issue => (
+ <ConnectedIssue
+ key={issue}
+ issueKey={issue}
+ onClick={this.handleIssueSelect}
+ selected={this.props.selectedIssue === issue}/>
+ ))}
+ </div>
+ )}
+ </td>
+ );
+ }
+
+ render () {
+ const { line, duplicationsCount, filtered } = this.props;
+ const className = classNames('source-line', {
+ 'source-line-highlighted': this.props.highlighted,
+ 'source-line-shadowed': filtered === false,
+ 'source-line-filtered': filtered === true
+ });
+
+ return (
+ <tr className={className} data-line-number={line.line}>
+ {this.renderLineNumber()}
+
+ {this.renderSCM()}
+
+ {this.props.displayCoverage && this.renderCoverage()}
+
+ {this.props.displayDuplications && this.renderDuplications()}
+
+ {duplicationsCount > 0 && this.renderDuplicationsExtra()}
+
+ {this.props.displayIssues && !this.props.displayAllIssues && this.renderIssuesIndicator()}
+
+ {this.props.displayFiltered && (
+ <td className="source-meta source-line-filtered-container" data-line-number={line.line}>
+ <div className="source-line-bar"/>
+ </td>
+ )}
+
+ {this.renderCode()}
+ </tr>
+ );
+ }
+}