/** * Provides a new overview of the cotents of the file, *semantically* */ /** imports */ import * as ui from "../../ui"; import * as csx from '../../base/csx'; import * as React from "react"; import * as ReactDOM from "react-dom"; import * as state from "../../state/state"; import { connect } from "react-redux"; import * as clipboard from "../../components/clipboard"; import * as utils from "../../../common/utils"; import { server } from "../../../socket/socketClient"; import { Types } from "../../../socket/socketContract"; import * as commands from "../../commands/commands"; import * as typestyle from "typestyle"; import * as styles from "../../styles/styles"; import { shouldComponentUpdate } from "../../../common/pure"; import { gotoPosition } from "../../monaco/monacoUtils"; type Editor = monaco.editor.ICommonCodeEditor; namespace SemanticViewStyles { export const root = { padding: '5px', background: '#343333', // Font color: '#BBB', fontSize: '.6rem', fontWeight: 'bold', // Overflow overflow: 'auto', // Limit width maxWidth: '200px', opacity: '0.7', // Light as this is not the user's focus transition: 'opacity .2s', ':hover': { opacity: '1' } } as any; export const nodeClass = typestyle.style(csx.extend(styles.ellipsis, { paddingTop: '2px', paddingBottom: '3px', paddingLeft: '2px', paddingRight: '2px', // Border which merges with the top marginTop: '-1px', border: '1px solid transparent', cursor: 'pointer', '-webkit-user-select': 'none', transition: 'background .2s', '&:hover': { backgroundColor: '#555' } })); export const selectedNodeClass = typestyle.style({ border: '1px solid grey', backgroundColor: styles.blackHighlightColor, $nest: { '&:hover': { backgroundColor: styles.blackHighlightColor } } }); } interface Props { editor?: Editor, filePath?: string, // Connected below showSemanticView?: boolean, } interface State { tree?: Types.SemanticTreeNode[]; cursor?: EditorPosition; } @connect((state: state.StoreState): Props => { return { showSemanticView: state.showSemanticView, }; }) export class SemanticView extends ui.BaseComponent { shouldComponentUpdate() { return shouldComponentUpdate.apply(this, arguments); } constructor(props) { super(props); this.state = { tree: [] } } componentWillUnmount() { super.componentWillUnmount(); } componentWillReceiveProps(props: Props) { if (props.showSemanticView && !this.props.showSemanticView) { this.handleCursorActivity(); } if (!this.props.editor && props.editor) { /** Initial data load */ let sel = props.editor.getSelection(); const cursor = { line: sel.startLineNumber - 1, ch: sel.endLineNumber - 1 }; this.setState({ cursor }); this.reloadData(); const reloadDataDebounced = utils.debounce(this.reloadData, 3000); this.disposible.add(commands.fileContentsChanged.on(e => { if (e.filePath === props.filePath && this.props.showSemanticView) { reloadDataDebounced(); } })); this.disposible.add(state.subscribeSub((s => s.showSemanticView), (showSemanticView) => { if (showSemanticView) { reloadDataDebounced(); } })); this.handleCursorActivity(props.editor); this.disposible.add(props.editor.onDidChangeCursorSelection(() => this.handleCursorActivity())); } /** * If just hidden and we have an editor, then the editor needs resizing */ if (!props.showSemanticView && this.props.showSemanticView && this.props.editor) { this.afterComponentDidUpdate(() => { this.relayoutEditor(); }); } } handleCursorActivity = utils.debounce((editor: Editor = this.props.editor) => { if (!editor) return; // Still loading or maybe unloaded if (!state.inActiveProjectFilePath(this.props.filePath)) return; if (this.isUnmounted) return; if (!this.props.showSemanticView) return; let sel = editor.getSelection(); const cursor = { line: sel.startLineNumber - 1, ch: sel.startColumn - 1 }; /** If first call OR cursor moved */ if (!this.state.cursor || (this.state.cursor && this.state.cursor.line !== cursor.line)) { this.setState({ cursor }); this.afterComponentDidUpdate(() => { // Scroll to select node in view if any const ref = this.refs[this.selectedRef] as HTMLDivElement; if (ref) { ref.scrollIntoViewIfNeeded(true); } }) } }, 1000); render() { if (!this.props.showSemanticView || !this.state.cursor) { return
; } if (!state.inActiveProjectFilePath(this.props.filePath)) { return
; } return
{this.state.tree.map(node => this.renderNode(node, 0))}
; } renderNode(node: Types.SemanticTreeNode, indent: number) { const isSelected = this.isSelected(node); const color = ui.kindToColor(node.kind); const ref = isSelected ? this.selectedRef.toString() : null; return [
{ this.gotoNode(node); event.stopPropagation(); } } data-start={node.start.line} data-end={node.end.line}> {ui.indent(indent)} {this.getIconForKind(node.kind)} {node.text}
].concat(node.subNodes.map(sn => this.renderNode(sn, indent + 1))); } gotoNode = (node: Types.SemanticTreeNode) => { const cursor = { line: node.start.line, ch: node.start.ch } const editor = this.props.editor; gotoPosition({ editor, position: cursor }); editor.focus(); this.setState({ cursor }); } getIconForKind(kind: string) { return ui.kindToIcon(kind); } /** Constantly incrementing so that this points to the last (deepest) selected node :) */ selectedRef: number = 0; isSelected = (node: Types.SemanticTreeNode) => { if (!this.state.cursor) return ''; const cursor = this.state.cursor; if (node.start.line <= cursor.line && node.end.line >= cursor.line) { this.selectedRef++; return SemanticViewStyles.selectedNodeClass; } return ''; } /** If the tree changes its width we need to relayout the editor */ lastWidth: number = 0; /** Loads the tree data */ reloadData = () => { if (!this.props.filePath) return; if (!state.inActiveProjectFilePath(this.props.filePath)) return; server.getSemanticTree({ filePath: this.props.filePath }).then(res => { this.afterComponentDidUpdate(() => { // also relayout the editor if the last width is not the same as new width const newWidth = ReactDOM.findDOMNode(this).clientWidth; if (this.lastWidth !== newWidth) { this.relayoutEditor(); this.lastWidth = newWidth; } }) this.setState({ tree: res.nodes }); }); } relayoutEditor() { // We store `top` (and restore it) otherwise the editor jumps around a bit after relayout. const top = this.props.editor.getScrollTop(); this.props.editor.layout(); this.props.editor.setScrollTop(top); } }