UNPKG

alm

Version:

The best IDE for TypeScript

629 lines (628 loc) 29.8 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var React = require("react"); var ReactDOM = require("react-dom"); var csx = require("../base/csx"); var ui_1 = require("../ui"); var ui = require("../ui"); var Modal = require("react-modal"); var styles = require("../styles/styles"); var utils_1 = require("../../common/utils"); var socketClient_1 = require("../../socket/socketClient"); var commands = require("../commands/commands"); var fuzzaldrin_1 = require("fuzzaldrin"); var icon_1 = require("../components/icon"); var events_1 = require("../../common/events"); var state = require("../state/state"); var types = require("../../common/types"); var robocop_1 = require("../components/robocop"); var utils = require("../../common/utils"); var appTabsContainer_1 = require("../tabs/v2/appTabsContainer"); var typestyle = require("typestyle"); var inputClassName = typestyle.style(styles.modal.inputStyleBase); /** Stuff shared by the select list view */ var selectListView_1 = require(".././selectListView"); var SearchMode; (function (SearchMode) { /** * Use if the user does something like `👎>` i.e. invalid mode key * This is also used to search for a mode */ SearchMode[SearchMode["Unknown"] = 0] = "Unknown"; SearchMode[SearchMode["File"] = 1] = "File"; SearchMode[SearchMode["Command"] = 2] = "Command"; SearchMode[SearchMode["Project"] = 3] = "Project"; SearchMode[SearchMode["Symbol"] = 4] = "Symbol"; SearchMode[SearchMode["FilesInProject"] = 5] = "FilesInProject"; })(SearchMode || (SearchMode = {})); var selectedStyle = { background: '#545454', color: 'white' }; var listItemStyle = { fontFamily: 'monospace', boxSizing: 'content-box' /** Otherwise the items don't expand to encapsulate children */ }; var searchingNameStyle = { marginTop: '0px', marginBottom: '0px', marginLeft: '10px', border: '1px solid grey', padding: '4px 4px', background: 'black' }; var OmniSearch = /** @class */ (function (_super) { __extends(OmniSearch, _super); function OmniSearch(props) { var _this = _super.call(this, props) || this; _this.mode = SearchMode.File; _this.searchState = new SearchState(); _this.wasShown = false; _this.setRawFilterValue = function (value) { // also scroll to the end of the input after loading var input = ReactDOM.findDOMNode(_this.refs.omniSearchInput); if (!input) return; input.value = value; var len = value.length; input.setSelectionRange(len, len); }; _this.onChangeFilter = utils_1.debounce(function (e) { var filterValue = ReactDOM.findDOMNode(_this.refs.omniSearchInput).value; _this.searchState.newValue(filterValue); }, 50); _this.incrementSelected = utils_1.debounce(function () { _this.searchState.incrementSelected(); }, 0, true); _this.decrementSelected = utils_1.debounce(function () { _this.searchState.decrementSelected(); }, 0, true); _this.onChangeSelected = function (event) { var keyStates = ui.getKeyStates(event); if (keyStates.up || keyStates.tabPrevious) { event.preventDefault(); _this.decrementSelected(); } if (keyStates.down || keyStates.tabNext) { event.preventDefault(); _this.incrementSelected(); } if (keyStates.enter) { event.preventDefault(); _this.searchState.choseIndex(_this.searchState.selectedIndex); } }; _this.searchState.stateChanged.on(function () { return _this.forceUpdate(); }); _this.searchState.setParentUiRawFilterValue = function (value) { _this.setRawFilterValue(value); }; _this.state = _this.propsToState(props); return _this; } OmniSearch.prototype.propsToState = function (props) { return {}; }; OmniSearch.prototype.componentWillReceiveProps = function (props) { this.setState(this.propsToState(props)); }; OmniSearch.prototype.componentDidMount = function () { var _this = this; commands.omniFindFile.on(function () { _this.searchState.openOmniSearch(SearchMode.File); }); commands.omniFindCommand.on(function () { _this.searchState.openOmniSearch(SearchMode.Command); }); commands.omniSelectProject.on(function () { _this.searchState.openOmniSearch(SearchMode.Project); }); commands.omniProjectSymbols.on(function () { _this.searchState.openOmniSearch(SearchMode.Symbol); }); commands.omniProjectSourcefile.on(function () { _this.searchState.openOmniSearch(SearchMode.FilesInProject); }); }; OmniSearch.prototype.componentWillUpdate = function () { this.wasShown = this.searchState.isShown; }; OmniSearch.prototype.componentDidUpdate = function () { var _this = this; // get the dom node that is selected // make sure its parent scrolls to make this visible setTimeout(function () { if (_this.refs.selected) { var selected = _this.refs.selected; selected.scrollIntoViewIfNeeded(false); } // also keep the input in focus if (_this.searchState.isShown) { var input = _this.refs.omniSearchInput; input.focus(); // and scroll to the end if its just been shown if (!_this.wasShown) { var len = input.value.length; input.setSelectionRange(len, len); _this.wasShown = true; } } }); }; OmniSearch.prototype.render = function () { var renderedResults = this.searchState.renderResults(); var searchingName = this.searchState.getSearchingName(); return React.createElement(Modal, { isOpen: this.searchState.isShown, onRequestClose: this.searchState.closeOmniSearch }, React.createElement("div", { style: csx.extend(csx.vertical, csx.flex) }, React.createElement("div", { style: csx.extend(csx.content, csx.horizontal, csx.center) }, React.createElement("h4", { style: { marginTop: '1rem', marginBottom: '1rem' } }, "Omni Search ", React.createElement(icon_1.Icon, { name: "search" })), searchingName ? React.createElement("h5", { style: searchingNameStyle }, searchingName) : '', React.createElement("div", { style: csx.flex }), React.createElement("div", { style: { fontSize: '0.9rem', color: 'grey' } }, React.createElement("code", { style: styles.modal.keyStrokeStyle }, "Esc"), " to exit ", React.createElement("code", { style: styles.modal.keyStrokeStyle }, "Enter"), " to select")), React.createElement("div", { style: csx.extend(csx.content, styles.padded1TopBottom, csx.vertical) }, React.createElement("input", { defaultValue: this.searchState.rawFilterValue, className: inputClassName, type: "text", ref: "omniSearchInput", placeholder: "Filter", onChange: this.onChangeFilter, onKeyDown: this.onChangeSelected })), this.searchState.optionalMessage(), React.createElement("div", { ref: "searchScroll", className: "scrollContainer", style: csx.extend(csx.vertical, csx.flex, { overflow: 'auto' }) }, renderedResults))); }; return OmniSearch; }(ui_1.BaseComponent)); exports.OmniSearch = OmniSearch; /** * Omni search has a lot of UI work (debouncing and whatnot) in it, * don't want to sprinkel in all the MODE selection stuff in there as well * So ... created this class * Responsible for taking user input > parsing it and then > returning the rendered search results * Also maintains the selected index within the search results and takes approriate action if user commits to it * * functions marched with "mode" contain mode specific logic */ var SearchState = /** @class */ (function () { function SearchState() { var _this = this; /** * Current raw user input value */ this.rawFilterValue = ''; this.parsedFilterValue = ''; /** * Various search lists */ /** filepath */ this.filePaths = []; this.filePathsCompleted = false; /** project */ this.availableProjects = []; /** commands */ this.commands = commands.commandRegistry; /** symols */ this.symbols = []; /** source code files */ this.filesPathsInProject = []; /** Modes can use this to store their results */ this.filteredValues = []; /** * Current mode */ this.mode = SearchMode.File; this.modeDescriptions = []; // set in ctor this.modeMap = {}; // set in ctor /** * showing search results */ this.isShown = false; /** * The currently selected search result */ this.selectedIndex = 0; /** * if there are new search results the user might care about, or user selection changed or whatever */ this.stateChanged = new events_1.TypedEvent(); /** If we change the raw user value */ this.setParentUiRawFilterValue = function (rawFilterValue) { return null; }; /** for performance reasons */ this.maxShowCount = 20; /** Mode */ this.choseIndex = function (index) { if (_this.mode == SearchMode.Unknown) { var modeDescription = _this.filteredValues[index]; _this.rawFilterValue = modeDescription.shortcut + '>'; _this.newValue(_this.rawFilterValue); _this.setParentUiRawFilterValue(_this.rawFilterValue); return; } if (_this.mode == SearchMode.File) { var filePath = _this.filteredValues[index]; var line = utils.getFilePathLine(_this.parsedFilterValue).line; if (filePath) { commands.doOpenFile.emit({ filePath: filePath, position: { line: line, ch: 0 } }); } _this.closeOmniSearch(); return; } if (_this.mode == SearchMode.Command) { var command = _this.filteredValues[index]; if (command) { command.emit({}); } if (command !== commands.omniFindFile && command !== commands.omniFindCommand && command !== commands.omniSelectProject) { _this.closeOmniSearch(); } return; } if (_this.mode == SearchMode.Project) { var activeProject = _this.filteredValues[index]; if (activeProject) { socketClient_1.server.setActiveProjectConfigDetails(activeProject); state.setActiveProject(activeProject); state.setFilePathsInActiveProject([]); } _this.closeOmniSearch(); return; } if (_this.mode == SearchMode.Symbol) { var symbol = _this.filteredValues[index]; if (symbol) { commands.doOpenOrFocusFile.emit({ filePath: symbol.filePath, position: symbol.position }); } _this.closeOmniSearch(); return; } if (_this.mode == SearchMode.FilesInProject) { var filePath = _this.filteredValues[index]; var line = utils.getFilePathLine(_this.parsedFilterValue).line; if (filePath) { commands.doOpenFile.emit({ filePath: filePath, position: { line: line, ch: 0 } }); } _this.closeOmniSearch(); return; } }; /** Mode */ this.openOmniSearch = function (mode) { var wasAlreadyShown = _this.isShown; _this.isShown = true; var oldMode = _this.mode; var oldRawFilterValue = _this.rawFilterValue; _this.mode = mode; var description = _this.modeDescriptions.filter(function (x) { return x.mode == mode; })[0]; _this.rawFilterValue = description ? description.shortcut + '>' : ''; // If already shown would be nice to preserve current user input // And if the new mode is different // And if the new mode is not *search* search mode if (wasAlreadyShown && oldMode !== _this.mode && oldMode !== SearchMode.Unknown) { _this.rawFilterValue = _this.rawFilterValue + oldRawFilterValue.trim().substr(2); } _this.newValue(_this.rawFilterValue, wasAlreadyShown, oldMode !== _this.mode); }; this.incrementSelected = function () { _this.selectedIndex = utils_1.rangeLimited({ num: _this.selectedIndex + 1, min: 0, max: _this.filteredValues.length - 1, loopAround: true }); _this.stateChanged.emit({}); }; this.decrementSelected = function () { _this.selectedIndex = utils_1.rangeLimited({ num: _this.selectedIndex - 1, min: 0, max: _this.filteredValues.length - 1, loopAround: true }); _this.stateChanged.emit({}); }; this.closeOmniSearch = function () { _this.isShown = false; _this.rawFilterValue = ''; _this.stateChanged.emit({}); }; commands.esc.on(function () { _this.closeOmniSearch(); }); this.filePaths = state.getState().filePaths.filter(function (fp) { return fp.type == types.FilePathType.File; }).map(function (fp) { return fp.filePath; }); state.subscribeSub(function (state) { return state.filePaths; }, function (filePaths) { _this.filePaths = filePaths.filter(function (fp) { return fp.type == types.FilePathType.File; }).map(function (fp) { return fp.filePath; }); _this.updateIfUserIsSearching(SearchMode.File); }); this.filePathsCompleted = state.getState().filePathsCompleted; state.subscribeSub(function (state) { return state.filePathsCompleted; }, function (filePathsCompleted) { _this.filePathsCompleted = filePathsCompleted; _this.updateIfUserIsSearching(SearchMode.File); }); socketClient_1.server.availableProjects({}).then(function (res) { _this.availableProjects = res; }); socketClient_1.cast.availableProjectsUpdated.on(function (res) { _this.availableProjects = res; }); this.modeDescriptions = [ { mode: SearchMode.File, description: 'Search for a File in the working directory', shortcut: 'f', searchingName: "Files", keyboardShortcut: commands.omniFindFile.config.keyboardShortcut }, { mode: SearchMode.Command, description: 'Search for a Command', shortcut: 'c', searchingName: "Commands", keyboardShortcut: commands.omniFindCommand.config.keyboardShortcut }, { mode: SearchMode.Project, description: 'Search for a TypeScript Project to work on', shortcut: 'p', searchingName: "Projects", keyboardShortcut: commands.omniSelectProject.config.keyboardShortcut }, { mode: SearchMode.Symbol, description: 'Search for Symbols (Hieroglyphs) in active project', shortcut: 'h', searchingName: "Symbols", keyboardShortcut: commands.omniProjectSymbols.config.keyboardShortcut }, { mode: SearchMode.FilesInProject, description: 'Search for TypeScript file in active project', shortcut: 't', searchingName: "Files In Project", keyboardShortcut: commands.omniProjectSourcefile.config.keyboardShortcut } ]; // setup mode map this.modeDescriptions.forEach(function (md) { return _this.modeMap[md.shortcut] = md.mode; }); } /** Mode */ SearchState.prototype.renderResults = function () { var _this = this; var renderedResults = []; if (this.mode == SearchMode.File) { var fileList = this.filteredValues; renderedResults = this.createRenderedForList(fileList, function (filePath) { // Create rendered var queryFilePath = utils.getFilePathLine(_this.parsedFilterValue).filePath; var renderedPath = selectListView_1.renderMatchedSegments(filePath, queryFilePath); var renderedFileName = selectListView_1.renderMatchedSegments(utils_1.getFileName(filePath), queryFilePath); return (React.createElement("div", null, React.createElement("div", null, renderedFileName), renderedPath)); }); } if (this.mode == SearchMode.FilesInProject) { var fileList = this.filteredValues; renderedResults = this.createRenderedForList(fileList, function (filePath) { // Create rendered var queryFilePath = utils.getFilePathLine(_this.parsedFilterValue).filePath; var renderedPath = selectListView_1.renderMatchedSegments(filePath, queryFilePath); var renderedFileName = selectListView_1.renderMatchedSegments(utils_1.getFileName(filePath), queryFilePath); return (React.createElement("div", null, React.createElement("div", null, renderedFileName), renderedPath)); }); } if (this.mode == SearchMode.Command) { var filtered = this.filteredValues; renderedResults = this.createRenderedForList(filtered, function (command) { // Create rendered var matched = selectListView_1.renderMatchedSegments(command.config.description, _this.parsedFilterValue); return (React.createElement("div", { style: csx.horizontal }, React.createElement("span", null, matched), React.createElement("span", { style: csx.flex }), command.config.keyboardShortcut && React.createElement("div", { style: commandKeyStrokeStyle }, commandShortcutToDisplayName(command.config.keyboardShortcut)))); }); } if (this.mode == SearchMode.Project) { var filteredProjects = this.filteredValues; renderedResults = this.createRenderedForList(filteredProjects, function (project) { // Create rendered var matched = selectListView_1.renderMatchedSegments(project.name, _this.parsedFilterValue); return (React.createElement("div", null, matched)); }); } if (this.mode == SearchMode.Symbol) { var filtered = this.filteredValues; renderedResults = this.createRenderedForList(filtered, function (symbol) { // Create rendered // NOTE: Code duplicated in `gotoTypeScriptSymbol.tsx` var matched = selectListView_1.renderMatchedSegments(symbol.name, _this.parsedFilterValue); var color = ui.kindToColor(symbol.kind); var icon = ui.kindToIcon(symbol.kind); return (React.createElement("div", null, React.createElement("div", { style: csx.horizontal }, React.createElement("span", null, matched), React.createElement("span", { style: csx.flex }), React.createElement("strong", { style: { color: color } }, symbol.kind), "\u00A0", React.createElement("span", { style: csx.extend({ color: color, fontFamily: 'FontAwesome' }) }, icon)), React.createElement("div", null, symbol.fileName, ":", symbol.position.line + 1))); }); } if (this.mode == SearchMode.Unknown) { var filtered = this.filteredValues; renderedResults = this.createRenderedForList(filtered, function (modeDescription) { // Create rendered var matched = selectListView_1.renderMatchedSegments(modeDescription.description, _this.parsedFilterValue); return (React.createElement("div", { style: csx.extend(csx.horizontal) }, React.createElement("div", null, modeDescription.shortcut, '>', " ", matched), React.createElement("span", { style: csx.flex }), React.createElement("div", { style: commandKeyStrokeStyle }, commandShortcutToDisplayName(modeDescription.keyboardShortcut)))); }); } return renderedResults; }; /** Mode */ SearchState.prototype.optionalMessage = function () { if (this.mode == SearchMode.File && !this.filePathsCompleted) { var messageStyle = { fontSize: '.6rem', textAlign: 'center', background: '#333', color: "#ddd", padding: '5px', fontWeight: 'bold', boxShadow: 'inset 0 0 6px black', }; return (React.createElement("div", { style: csx.content }, React.createElement("div", { style: messageStyle }, "Indexing (", this.filePaths.length, ")"), React.createElement(robocop_1.Robocop, null))); } return null; }; /** Mode */ /** This is the heart of the processing .. ensuring state consistency */ SearchState.prototype.newValue = function (value, wasShownBefore, modeChanged) { var _this = this; if (wasShownBefore === void 0) { wasShownBefore = true; } if (modeChanged === void 0) { modeChanged = false; } this.rawFilterValue = value; var oldMode = this.mode; // Parse the query to see what type it is this.parsedFilterValue = ''; var trimmed = value.trim(); if (trimmed.length > 1 && trimmed[1] == '>') { var mode = this.modeMap[trimmed[0]]; if (!mode) { this.mode = SearchMode.Unknown; this.parsedFilterValue = trimmed; } else { this.mode = mode; this.parsedFilterValue = trimmed.substr(2); } } else { this.mode = SearchMode.Unknown; this.parsedFilterValue = trimmed; } modeChanged = modeChanged || this.mode !== oldMode; var modeChangedOrJustOpened = modeChanged || !wasShownBefore; var potentiallyRefreshModeData = modeChangedOrJustOpened ? this.refreshModeData() : Promise.resolve({}); potentiallyRefreshModeData.then(function () { if (_this.mode == SearchMode.Unknown) { _this.filteredValues = _this.parsedFilterValue ? selectListView_1.getFilteredItems({ items: _this.modeDescriptions, textify: function (c) { return c.description; }, filterValue: _this.parsedFilterValue }) : _this.modeDescriptions; } if (_this.mode == SearchMode.File) { var filePath = utils.getFilePathLine(_this.parsedFilterValue).filePath; _this.filteredValues = fuzzaldrin_1.filter(_this.filePaths, filePath); _this.filteredValues = _this.filteredValues.slice(0, _this.maxShowCount); } if (_this.mode == SearchMode.FilesInProject) { var filePath = utils.getFilePathLine(_this.parsedFilterValue).filePath; _this.filteredValues = fuzzaldrin_1.filter(_this.filesPathsInProject, filePath); } if (_this.mode == SearchMode.Command) { _this.filteredValues = _this.parsedFilterValue ? selectListView_1.getFilteredItems({ items: _this.commands, textify: function (c) { return c.config.description; }, filterValue: _this.parsedFilterValue }) : _this.commands; } if (_this.mode == SearchMode.Project) { // only add a virtual project if the active file path is a .ts or .js file that isn't in active project var availableProjects = _this.availableProjects.slice(); var tab = appTabsContainer_1.tabState.getSelectedTab(); var filePath = tab && utils.getFilePathFromUrl(tab.url); if (filePath && utils.isJsOrTs(filePath) && !state.inActiveProjectUrl(tab.url)) { availableProjects.unshift({ name: "Virtual: " + utils.getFileName(filePath), isVirtual: true, tsconfigFilePath: filePath }); } _this.filteredValues = _this.parsedFilterValue ? selectListView_1.getFilteredItems({ items: availableProjects, textify: function (p) { return p.name; }, filterValue: _this.parsedFilterValue }) : availableProjects; } if (_this.mode == SearchMode.Symbol) { _this.filteredValues = _this.parsedFilterValue ? selectListView_1.getFilteredItems({ items: _this.symbols, textify: function (p) { return p.name; }, filterValue: _this.parsedFilterValue }) : _this.symbols; _this.filteredValues = _this.filteredValues.slice(0, _this.maxShowCount); } _this.selectedIndex = 0; _this.stateChanged.emit({}); if (modeChangedOrJustOpened) { _this.setParentUiRawFilterValue(_this.rawFilterValue); } }); }; /** Mode */ SearchState.prototype.refreshModeData = function () { var _this = this; // If the new mode requires a search we do that here if (this.mode == SearchMode.Symbol) { return socketClient_1.server.getNavigateToItems({}).then(function (res) { _this.symbols = res.items; }); } if (this.mode == SearchMode.FilesInProject) { this.filesPathsInProject = state.getState().filePathsInActiveProject; } return Promise.resolve(); }; SearchState.prototype.getSearchingName = function () { var _this = this; if (this.mode == SearchMode.Unknown) { return 'Modes'; } var description = this.modeDescriptions.filter(function (x) { return x.mode == _this.mode; })[0]; if (!description) return ''; else return description.searchingName; }; SearchState.prototype.createRenderedForList = function (items, itemToRender) { var _this = this; return items.map(function (item, index) { var rendered = itemToRender(item); var selected = _this.selectedIndex === index; var style = selected ? selectedStyle : {}; var ref = selected && "selected"; return (React.createElement("div", { key: index, style: csx.extend(style, styles.padded2, styles.hand, listItemStyle), onClick: function () { return _this.choseIndex(index); }, ref: ref }, rendered)); }); }; SearchState.prototype.updateIfUserIsSearching = function (mode) { if (this.mode == mode && this.isShown) { this.newValue(this.rawFilterValue); } }; return SearchState; }()); var commandKeyStrokeStyle = { fontSize: '.7rem', color: '#DDD', background: '#111', paddingLeft: '4px', paddingRight: '4px', border: '2px solid', borderRadius: '4px', }; /** Utility function for command display */ function commandShortcutToDisplayName(shortcut) { var basic = shortcut .replace(/mod/g, commands.modName) .replace(/alt/g, 'Alt') .replace(/shift/g, 'Shift'); var onPlus = basic.split('+'); onPlus[onPlus.length - 1] = onPlus[onPlus.length - 1].toUpperCase(); return onPlus.join(' + '); }