import React = require("react"); import ReactDOM = require("react-dom"); import * as csx from '../../base/csx'; import {BaseComponent} from "../../ui"; import * as ui from "../../ui"; import * as utils from "../../../common/utils"; import * as styles from "../../styles/styles"; import * as state from "../../state/state"; import * as uix from "../../uix"; import * as commands from "../../commands/commands"; import Modal = require('react-modal'); import {server} from "../../../socket/socketClient"; import {Types} from "../../../socket/socketContract"; import {modal} from "../../styles/styles"; import {Robocop} from "../../components/robocop"; import * as docCache from "../model/docCache"; import {CodeEditor} from "../editor/codeEditor"; import {RefactoringsByFilePath, Refactoring} from "../../../common/types"; import * as typestyle from "typestyle"; const inputClassName = typestyle.style(styles.modal.inputStyleBase); export interface Props { info: Types.GetRenameInfoResponse; alreadyOpenFilePaths: string[]; currentlyClosedFilePaths: string[]; unmount: () => any; } export interface State { invalidMessage?: string; selectedIndex?: number; flattened?: { filePath: string, preview: ts.TextSpan, indexForFilePath: number, totalForFilePath: number }[]; } let validationErrorStyle = { color: 'red', fontFamily: 'monospace', fontSize: '1.2rem', padding: '5px', } let summaryStyle = { padding: '5px', backgroundColor: '#222', color: '#CCC', fontSize: '.8rem', } export class RenameVariable extends BaseComponent{ constructor(props: Props) { super(props); let flattended = utils.selectMany(Object.keys(props.info.locations).map(filePath => { let refs = props.info.locations[filePath].slice().reverse(); return refs.map((preview,i) => { return { filePath, preview, indexForFilePath: i + 1, totalForFilePath: refs.length, }; }); })); this.state = { selectedIndex: 0, flattened: flattended }; } componentDidMount() { this.disposible.add(commands.esc.on(() => { this.props.unmount(); })); setTimeout(() => { this.focus(); let input = (ReactDOM.findDOMNode(this.refs.mainInput) as HTMLInputElement) let len = input.value.length; input.setSelectionRange(0, len); }); } componentDidUpdate() { setTimeout(() => { let selected = ReactDOM.findDOMNode(this.refs.selectedTabTitle) as HTMLDivElement; if (selected) { selected.scrollIntoViewIfNeeded(false); } }); } refs: { [string: string]: any; mainInput: any; selectedTabTitle: any; } render() { let selectedPreview = this.state.flattened[this.state.selectedIndex]; let filePathsRendered = this.state.flattened.map((item,i)=>{ let selected = i == this.state.selectedIndex; let active = selected ? styles.tabHeaderActive : {}; let ref = selected && "selectedTabTitle"; return (
this.selectAndRefocus(i)}>
{utils.getFileName(item.filePath)} ({item.indexForFilePath} of {item.totalForFilePath})
); }); let previewRendered = ; return (

Rename

Esc to exit Enter to select {' '}Up / Down to see usages
{ this.state.invalidMessage &&
{this.state.invalidMessage}
}
{this.state.flattened.length} usages, {this.props.alreadyOpenFilePaths.length} files open, {this.props.currentlyClosedFilePaths.length} files closed
{filePathsRendered}
{previewRendered}
); } onChangeFilter = (e) => { let newText = (ReactDOM.findDOMNode(this.refs.mainInput) as HTMLInputElement).value; if (newText.replace(/\s/g, '') !== newText.trim()) { this.setState({ invalidMessage: 'The new variable must not contain a space' }); } else if (!newText.trim()) { this.setState({ invalidMessage: 'Please provide a new name or press esc to abort rename' }); } else { this.setState({ invalidMessage: '' }); } }; onChangeSelected = (event) => { let keyStates = ui.getKeyStates(event); if (keyStates.up || keyStates.tabPrevious) { event.preventDefault(); let selectedIndex = utils.rangeLimited({ num: this.state.selectedIndex - 1, min: 0, max: this.state.flattened.length - 1, loopAround: true }); this.setState({selectedIndex}); } if (keyStates.down || keyStates.tabNext) { event.preventDefault(); let selectedIndex = utils.rangeLimited({ num: this.state.selectedIndex + 1, min: 0, max: this.state.flattened.length - 1, loopAround: true }); this.setState({selectedIndex}); } if (keyStates.enter) { event.preventDefault(); let newText = (ReactDOM.findDOMNode(this.refs.mainInput) as HTMLInputElement).value.trim(); let refactorings: RefactoringsByFilePath = {}; Object.keys(this.props.info.locations).map(filePath => { refactorings[filePath] = []; let forPath = refactorings[filePath]; this.props.info.locations[filePath].forEach(loc=>{ let refactoring: Refactoring = { filePath: filePath, span: loc, newText } forPath.push(refactoring); }); }); uix.API.applyRefactorings(refactorings); setTimeout(()=>{this.props.unmount()}); } }; selectAndRefocus = (index: number) => { this.setState({selectedIndex:index}); this.focus(); }; focus = () => { let input = (ReactDOM.findDOMNode(this.refs.mainInput) as HTMLInputElement) input.focus(); } } import * as monacoUtils from "../monacoUtils"; import CommonEditorRegistry = monaco.CommonEditorRegistry; import ICommonCodeEditor = monaco.ICommonCodeEditor; import TPromise = monaco.Promise; import EditorAction = monaco.EditorAction; import KeyMod = monaco.KeyMod; import KeyCode = monaco.KeyCode; import ServicesAccessor = monaco.ServicesAccessor; import IActionOptions = monaco.IActionOptions; import EditorContextKeys = monaco.EditorContextKeys; class RenameVariableAction extends EditorAction { constructor() { super({ id: 'editor.action.renameVariable', label: 'TypeScript: Rename Variable', alias: 'TypeScript: Rename Variable', precondition: EditorContextKeys.Writable, kbOpts: { kbExpr: EditorContextKeys.TextFocus, primary: KeyCode.F2 } }); } public run(accessor:ServicesAccessor, editor:ICommonCodeEditor): void | TPromise { const filePath = editor.filePath; if (!state.inActiveProjectFilePath(filePath)) { ui.notifyInfoNormalDisappear('The current file is no in the active project'); return; } let position = monacoUtils.getCurrentPosition(editor); server.getRenameInfo({filePath,position}).then((res)=>{ if (!res.canRename){ ui.notifyInfoNormalDisappear("Rename not available at cursor location"); } else { let filePaths = Object.keys(res.locations); // if there is only a single file path and that is the current and there aren't that many usages // we do the rename inline if (filePaths.length == 1 && filePaths[0] == filePath && res.locations[filePath].length < 5) { selectName(editor, res.locations[filePath]); } else { let {alreadyOpenFilePaths, currentlyClosedFilePaths} = uix.API.getClosedVsOpenFilePaths(filePaths); const {node,unmount} = ui.getUnmountableNode(); ReactDOM.render(, node); } } }); } } CommonEditorRegistry.registerEditorAction(new RenameVariableAction()); /** Selects the locations keeping the current one as the first (to allow easy escape back to current cursor) */ function selectName(editor: monaco.ICommonCodeEditor, locations: ts.TextSpan[]) { const ranges: monaco.ISelection[] = []; const curPos = editor.getSelection(); for (var i = 0; i < locations.length; i++) { var ref = locations[i]; let from = editor.getModel().getPositionAt(ref.start); let to = editor.getModel().getPositionAt(ref.start + ref.length); const range = new monaco.Range(from.lineNumber, from.column, to.lineNumber, to.column); const selection: monaco.ISelection = { selectionStartLineNumber: from.lineNumber, selectionStartColumn: from.column, positionLineNumber: to.lineNumber, positionColumn: to.column } if (!monaco.Range.containsRange(range, curPos)) ranges.push(selection); else ranges.unshift(selection); } editor.setSelections(ranges); }