/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at . */
// @flow
import { PureComponent } from "react";
import {
toEditorPosition,
getDocument,
hasDocument,
startOperation,
endOperation,
getTokenEnd,
} from "../../utils/editor";
import { isException } from "../../utils/pause";
import { getIndentation } from "../../utils/indentation";
import { connect } from "../../utils/connect";
import {
getVisibleSelectedFrame,
getPauseReason,
getSourceWithContent,
getCurrentThread,
getPausePreviewLocation,
} from "../../selectors";
import type { SourceLocation, Why, SourceWithContent } from "../../types";
type OwnProps = {||};
type Props = {
location: ?SourceLocation,
why: ?Why,
source: ?SourceWithContent,
};
type TextClasses = {
markTextClass: string,
lineClass: string,
};
function isDocumentReady(
source: ?SourceWithContent,
location: ?SourceLocation
) {
return location && source && source.content && hasDocument(location.sourceId);
}
export class DebugLine extends PureComponent {
debugExpression: null;
componentDidMount() {
const { why, location, source } = this.props;
this.setDebugLine(why, location, source);
}
componentWillUnmount() {
const { why, location, source } = this.props;
this.clearDebugLine(why, location, source);
}
componentDidUpdate(prevProps: Props) {
const { why, location, source } = this.props;
startOperation();
this.clearDebugLine(prevProps.why, prevProps.location, prevProps.source);
this.setDebugLine(why, location, source);
endOperation();
}
setDebugLine(
why: ?Why,
location: ?SourceLocation,
source: ?SourceWithContent
) {
if (!location || !isDocumentReady(source, location)) {
return;
}
const { sourceId } = location;
const doc = getDocument(sourceId);
let { line, column } = toEditorPosition(location);
let { markTextClass, lineClass } = this.getTextClasses(why);
doc.addLineClass(line, "wrapClass", lineClass);
const lineText = doc.getLine(line);
column = Math.max(column, getIndentation(lineText));
// If component updates because user clicks on
// another source tab, codeMirror will be null.
const columnEnd = doc.cm ? getTokenEnd(doc.cm, line, column) : null;
if (columnEnd === null) {
markTextClass += " to-line-end";
}
this.debugExpression = doc.markText(
{ ch: column, line },
{ ch: columnEnd, line },
{ className: markTextClass }
);
}
clearDebugLine(
why: ?Why,
location: ?SourceLocation,
source: ?SourceWithContent
) {
if (!location || !isDocumentReady(source, location)) {
return;
}
if (this.debugExpression) {
this.debugExpression.clear();
}
const { line } = toEditorPosition(location);
const doc = getDocument(location.sourceId);
const { lineClass } = this.getTextClasses(why);
doc.removeLineClass(line, "wrapClass", lineClass);
}
getTextClasses(why: ?Why): TextClasses {
if (why && isException(why)) {
return {
markTextClass: "debug-expression-error",
lineClass: "new-debug-line-error",
};
}
return { markTextClass: "debug-expression", lineClass: "new-debug-line" };
}
render() {
return null;
}
}
const mapStateToProps = state => {
const frame = getVisibleSelectedFrame(state);
const previewLocation = getPausePreviewLocation(state);
const location = previewLocation || frame?.location;
return {
frame,
location,
source: location && getSourceWithContent(state, location.sourceId),
why: getPauseReason(state, getCurrentThread(state)),
};
};
export default connect(mapStateToProps)(DebugLine);