alm
Version:
The best IDE for TypeScript
629 lines (628 loc) • 29.8 kB
JavaScript
"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(' + ');
}