Skip to content

Commit fbb553d

Browse files
authored
Fuzzy search (codesandbox#123)
* Add dependencies * Add fuzzy search * Fix module clicking * Remove debug statement * Remove console.log of monaco en editor * Add fuzzy file search to embed
1 parent 2bfddf3 commit fbb553d

File tree

7 files changed

+351
-10
lines changed

7 files changed

+351
-10
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"cssnano": "^3.10.0",
9595
"custom-prettier-codesandbox": "CompuIves/custom-prettier-codesandbox",
9696
"debug": "^2.6.8",
97+
"downshift": "^1.0.0-rc.14",
9798
"eslint-config-react-app": "^1.0.5",
9899
"file-saver": "^1.3.3",
99100
"glamor": "^2.20.25",
@@ -104,6 +105,7 @@
104105
"humps": "CompuIves/humps",
105106
"jszip": "^3.1.3",
106107
"lodash": "^4.17.2",
108+
"match-sorter": "^1.8.1",
107109
"moment": "^2.18.1",
108110
"monaco-editor": "CompuIves/codesandbox-monaco-editor",
109111
"normalize.css": "^5.0.0",

src/app/components/sandbox/CodeEditor/CodeMirror.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'codemirror/addon/hint/show-hint';
1212
import 'codemirror/addon/tern/tern';
1313

1414
import Header from './Header';
15+
import FuzzySearch from './FuzzySearch';
1516

1617
const documentCache = {};
1718

@@ -26,6 +27,7 @@ type Props = {
2627
canSave: boolean,
2728
preferences: Preferences,
2829
onlyViewMode: boolean,
30+
setCurrentModule: ?(sandboxId: string, moduleId: string) => void,
2931
};
3032

3133
const Container = styled.div`
@@ -47,6 +49,7 @@ const fontFamilies = (...families) =>
4749
.join(', ');
4850

4951
const CodeContainer = styled.div`
52+
position: relative;
5053
overflow: auto;
5154
width: 100%;
5255
height: calc(100% - 6rem);
@@ -185,8 +188,16 @@ const handleError = (
185188
}
186189
};
187190

188-
export default class CodeEditor extends React.PureComponent<Props> {
191+
export default class CodeEditor extends React.PureComponent<Props, State> {
192+
state = {
193+
fuzzySearchEnabled: false,
194+
};
195+
189196
shouldComponentUpdate(nextProps: Props) {
197+
if (nextState.fuzzySearchEnabled !== this.state.fuzzySearchEnabled) {
198+
return true;
199+
}
200+
190201
return (
191202
nextProps.id !== this.props.id ||
192203
nextProps.errors !== this.props.errors ||
@@ -304,6 +315,9 @@ export default class CodeEditor extends React.PureComponent<Props> {
304315
cm.toggleComment({ lineComment: '//' });
305316
});
306317
},
318+
'Cmd-P': cm => {
319+
this.setState({ fuzzySearchEnabled: true });
320+
},
307321
};
308322

309323
const updateArgHints = cm => {
@@ -454,11 +468,30 @@ export default class CodeEditor extends React.PureComponent<Props> {
454468
});
455469
};
456470

471+
closeFuzzySearch = () => {
472+
this.setState({ fuzzySearchEnabled: false });
473+
};
474+
475+
setCurrentModule = moduleId => {
476+
this.closeFuzzySearch();
477+
if (this.props.setCurrentModule) {
478+
this.props.setCurrentModule(this.props.sandboxId, moduleId);
479+
}
480+
};
481+
457482
codemirror: typeof CodeMirror;
458483
server: typeof CodeMirror.TernServer;
459484

460485
render() {
461-
const { canSave, onlyViewMode, modulePath, preferences } = this.props;
486+
const {
487+
canSave,
488+
onlyViewMode,
489+
modulePath,
490+
preferences,
491+
modules,
492+
directories,
493+
id,
494+
} = this.props;
462495

463496
return (
464497
<Container>
@@ -471,6 +504,14 @@ export default class CodeEditor extends React.PureComponent<Props> {
471504
fontFamily={preferences.fontFamily}
472505
lineHeight={preferences.lineHeight}
473506
>
507+
{this.state.fuzzySearchEnabled &&
508+
<FuzzySearch
509+
closeFuzzySearch={this.closeFuzzySearch}
510+
setCurrentModule={this.setCurrentModule}
511+
modules={modules}
512+
directories={directories}
513+
currentModuleId={id}
514+
/>}
474515
<div
475516
style={{ height: '100%', fontSize: preferences.fontSize || 14 }}
476517
ref={this.getCodeMirror}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import * as React from 'react';
2+
import styled, { css } from 'styled-components';
3+
import { sortBy, groupBy, flatten } from 'lodash';
4+
import Downshift from 'downshift';
5+
import matchSorter from 'match-sorter';
6+
import type { Module, Directory } from 'common/types';
7+
8+
import { getModulePath } from 'app/store/entities/sandboxes/modules/selectors';
9+
import Input from 'app/components/Input';
10+
11+
import EntryIcons from 'app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/EntryIcons';
12+
import getType from 'app/store/entities/sandboxes/modules/utils/get-type';
13+
14+
const Container = styled.div`
15+
position: absolute;
16+
17+
top: 0;
18+
left: 0;
19+
right: 0;
20+
21+
z-index: 60;
22+
23+
margin: auto;
24+
padding-bottom: 0.25rem;
25+
26+
background-color: ${props => props.theme.background};
27+
28+
max-width: 650px;
29+
width: 100%;
30+
31+
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.6);
32+
`;
33+
34+
const InputContainer = styled.div`
35+
padding: 0.5rem;
36+
input {
37+
width: 100%;
38+
}
39+
`;
40+
41+
const Items = styled.div`
42+
max-height: 500px;
43+
overflow-y: auto;
44+
overflow-x: hidden;
45+
`;
46+
47+
const Entry = styled.div`
48+
position: relative;
49+
display: flex;
50+
align-items: center;
51+
font-size: .875rem;
52+
padding: 0.25rem 0.75rem;
53+
cursor: pointer;
54+
55+
${({ isNotSynced }) =>
56+
isNotSynced &&
57+
css`
58+
padding-left: 2rem;
59+
`} color: rgba(255, 255, 255, 0.8);
60+
61+
${({ isActive }) =>
62+
isActive &&
63+
css`
64+
background-color: ${props => props.theme.secondary.clearer(0.7)}
65+
`};
66+
`;
67+
68+
const CurrentModuleText = styled.div`
69+
position: absolute;
70+
right: 0.75rem;
71+
font-weight: 500;
72+
color: ${props => props.theme.secondary};
73+
`;
74+
75+
const Name = styled.span`margin: 0 0.5rem;`;
76+
77+
const Path = styled.span`
78+
margin: 0 0.25rem;
79+
font-weight: 400;
80+
color: rgba(255, 255, 255, 0.5);
81+
`;
82+
83+
type Props = {
84+
modules: Array<Module>,
85+
directories: Array<Directory>,
86+
setCurrentModule: (moduleId: string) => void,
87+
closeFuzzySearch: () => void,
88+
currentModuleId: string,
89+
};
90+
91+
export default class FuzzySearch extends React.PureComponent<Props> {
92+
// This is a precached map of paths to module
93+
paths = {};
94+
95+
componentWillMount() {
96+
const { modules, directories } = this.props;
97+
const modulePathData = modules.map(m => {
98+
const path = getModulePath(modules, directories, m.id);
99+
return {
100+
m,
101+
path,
102+
depth: path.split('/').length,
103+
};
104+
});
105+
106+
const groupedPaths = groupBy(modulePathData, n => n.depth);
107+
const sortedPaths = Object.values(groupedPaths).map(group =>
108+
sortBy(group, n => n.path),
109+
);
110+
const flattenedPaths = flatten(sortedPaths);
111+
112+
this.paths = flattenedPaths.reduce(
113+
(paths, { m, path }) => ({
114+
...paths,
115+
[m.id]: { path: path.replace('/', ''), m },
116+
}),
117+
{},
118+
);
119+
}
120+
121+
itemToString = m => (m ? m.path : '');
122+
123+
getItems = (value = '') => {
124+
const pathArray = Object.keys(this.paths).map(id => this.paths[id]);
125+
126+
return matchSorter(pathArray, value, { keys: ['path'] });
127+
};
128+
129+
onChange = item => {
130+
this.props.setCurrentModule(item.m.id);
131+
};
132+
133+
handleKeyUp = e => {
134+
if (e.keyCode === 27) {
135+
this.props.closeFuzzySearch();
136+
}
137+
};
138+
139+
render() {
140+
const { currentModuleId } = this.props;
141+
return (
142+
<Container>
143+
<Downshift
144+
defaultHighlightedIndex={0}
145+
defaultIsOpen
146+
onChange={this.onChange}
147+
itemToString={this.itemToString}
148+
>
149+
{({
150+
getInputProps,
151+
getItemProps,
152+
selectedItem,
153+
inputValue,
154+
highlightedIndex,
155+
}) =>
156+
<div style={{ width: '100%' }}>
157+
<InputContainer>
158+
<Input
159+
{...getInputProps({
160+
innerRef: el => el && el.focus(),
161+
onKeyUp: this.handleKeyUp,
162+
// Timeout so the fuzzy handler can still select the module
163+
onBlur: () => setTimeout(this.props.closeFuzzySearch, 100),
164+
})}
165+
/>
166+
</InputContainer>
167+
<Items>
168+
{this.getItems(inputValue).map((item, index) =>
169+
<Entry
170+
{...getItemProps({
171+
item,
172+
index,
173+
isActive: highlightedIndex === index,
174+
isSelected: selectedItem === item,
175+
})}
176+
key={item.m.id}
177+
isNotSynced={item.m.isNotSynced}
178+
>
179+
<EntryIcons
180+
isNotSynced={item.m.isNotSynced}
181+
type={getType(item.m)}
182+
error={item.m.errors && item.m.errors.length > 0}
183+
/>
184+
<Name>
185+
{item.m.title}
186+
</Name>
187+
{item.m.title !== this.itemToString(item) &&
188+
<Path>
189+
{this.itemToString(item)}
190+
</Path>}
191+
{item.m.id === currentModuleId &&
192+
<CurrentModuleText>currently opened</CurrentModuleText>}
193+
</Entry>,
194+
)}
195+
</Items>
196+
</div>}
197+
</Downshift>
198+
</Container>
199+
);
200+
}
201+
}

0 commit comments

Comments
 (0)