Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search file & search text #6

Merged
merged 5 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ export function minus(a, b) {
export default SingleIDE;
```

#### 搜索文件
快捷键:command/ctrl + p

#### 搜索文本
快捷键:shift + command/ctrl + f

### 组件参数及方法

Expand Down
50 changes: 50 additions & 0 deletions src/components/searchfile/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.search-file-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 100px;
}

.search-file-body-back {
width: 50%;
padding: 20px;
background: var(--monaco-editor-background);
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.search-file-input {
width: calc(100% - 20px);
padding: 10px;
background: var(--monaco-list-focusBackground);
color: var(--monaco-list-focusForeground)
}

.search-file-input {
width: calc(100% - 20px);
padding: 10px;
background: var(--monaco-list-focusBackground);
color: var(--monaco-list-focusForeground)
}

.search-file-result-item-selected,
.search-file-result-item {
cursor: pointer;
line-height: 50px;
padding-left: 10px;
user-select: none;
color: var(--monaco-list-focusForeground)
}

.search-file-result-item-selected {
background: var(--monaco-list-focusBackground);
color: var(--monaco-list-focusForeground)
}

43 changes: 43 additions & 0 deletions src/components/searchfile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState, useEffect, useCallback } from 'react';
import SearchFileBody from './search-file-body';
import './index.less';

interface SearchFileProps {
list: string[];
onSelectFile: (filename: string) => void;
}

const SearchFile: React.FC<SearchFileProps> = (props) => {
const [searchResults, setSearchResults] = useState<string[]>([]);
const filenames = useState(Array.isArray(props.list) ? [...props.list] : [])[0];
const onSelectFile = props.onSelectFile;

const handleSearch = (query: string) => {
if (!query || query.length === 0) {
setSearchResults([]);
return;
}
const results = filenames.filter((file) =>
file.toLowerCase().includes(query.toLowerCase())
);
setSearchResults(results);
};

const onExecute = (filename: string) => {
onSelectFile && onSelectFile(filename);
};

return (
<div className='search-file-background'>
<div className='search-file-body-back'>
<SearchFileBody
onSearch={handleSearch}
searchResults={searchResults}
onExecute={onExecute}
/>
</div>
</div>
);
};

export default SearchFile;
100 changes: 100 additions & 0 deletions src/components/searchfile/search-file-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState, useRef, useEffect, ChangeEvent, KeyboardEvent } from 'react';
import Icon from '@components/icons';

interface SearchModalProps {
onSearch: (query: string) => void;
searchResults: string[];
onExecute: (result: string) => void;
}

const SearchModal: React.FC<SearchModalProps> = ({onSearch, searchResults, onExecute }) => {
const [searchQuery, setSearchQuery] = useState<string>('');
const [selectedItem, setSelectedItem] = useState<number>(0);
const inputRef = useRef<HTMLInputElement | null>(null);
const modalRef = useRef<HTMLUListElement | null>(null);

useEffect(() => {
inputRef.current?.focus();
}, [inputRef]);

useEffect(() => {
if (searchResults.length > 0 && modalRef.current) {
const selectedItemElement = modalRef.current.childNodes[selectedItem] as HTMLElement;
if (selectedItemElement) {
selectedItemElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [selectedItem, searchResults]);

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.id === 'file-search-input') {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedItem((prev) => (prev + 1) % searchResults.length);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedItem((prev) => (prev - 1 + searchResults.length) % searchResults.length);
} else if (e.key === 'Enter') {
e.preventDefault();
onExecute(searchResults[selectedItem]);
}
}
};

const onClickLine = (index: number) => {
setSelectedItem(index);
onExecute(searchResults[index]);
};

const getFileType = (filename: string) => {
const fileName = filename.split('/').pop();
let fileType;
if (fileName && fileName.indexOf('.') !== -1) {
fileType = `file_type_${fileName.split('.').slice(-1)}`;
} else {
fileType = 'default_file';
}
return fileType;
};

return (
<div className="search-file-modal-overlay">
<div className="search-file-modal" onClick={(e) => e.stopPropagation()}>
<input
ref={inputRef}
className='search-file-input'
id="file-search-input"
type="text"
value={searchQuery}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setSelectedItem(0);
onSearch(e.target.value);
}}
onKeyDown={handleKeyDown}
placeholder="Search for files..."
/>
{searchResults.length > 0 && (
<ul ref={modalRef} style={{ listStyleType: 'none', padding: 0, maxHeight: '500px', overflowY: 'auto' }}>
{searchResults.map((result, index) => (
<li
className= {index === selectedItem ? 'search-file-result-item-selected' : 'search-file-result-item'}
key={index}
onClick={() => onClickLine(index)}
>
<Icon type={getFileType(result)} style={{
marginLeft: '5px',
marginRight: '5px',
}} />
{result}
</li>
))}
</ul>
)}
</div>
</div>
);
};

export default SearchModal;
173 changes: 173 additions & 0 deletions src/components/searchtext/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import SearchInput from './search-input';
import SearchResult from './search-result';
import './search-text.less'

interface SearchAndReplaceProps {
onSelectedLine: (title: string, line: number) => void;
listFiles: Record<string, string>;
style?: React.CSSProperties;
onClose: React.Dispatch<React.SetStateAction<boolean>>;
}

interface SelectedRow {
titleIndex: number;
rowIndex: number;
}

type SearchResultType = Record<string, { code: string; line: number }[]>[];

const SearchAndReplace: React.FC<SearchAndReplaceProps> = ({onSelectedLine, listFiles, style, onClose}) => {
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState<SearchResultType>([]);
const [unExpandedTitles, setUnExpandedTitles] = useState<Record<number, boolean>>({});
const [selectedRow, setSelectedRow] = useState<SelectedRow>({ titleIndex: -1, rowIndex: -1 });
const [allSelectResults, setAllSelectResults] = useState<{ titleIndex: number; rowIndex: number }[]>([]);

const innerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
handleSearch();
}, [searchText, listFiles]);

useEffect(() => {
smoothSelectedResults();
}, [searchResults, unExpandedTitles]);

const clear = useCallback(() => {
setSearchResults([]);
setUnExpandedTitles({});
}, []);

const handleSearch = useCallback(() => {
if (searchText.length === 0) {
clear();
return;
}

var lsearchResults : SearchResultType = [];
for (const [key, value] of Object.entries(listFiles)) {
const matches = value.split("\n");
if (matches) {
var matchingSubstrings = [];
for (var i = 0; i < matches.length; i++) {
const lineStr = matches[i];
if (lineStr.toLowerCase().includes(searchText.toLowerCase())) {
matchingSubstrings.push({code: lineStr, line: i + 1});
}
}

if (matchingSubstrings.length > 0) {
//[key]
lsearchResults.push({[key]:matchingSubstrings});
}
}
}
setSearchResults(lsearchResults);
} , [searchText, listFiles]);

const smoothSelectedResults = useCallback(() => {
const selectedResults: { titleIndex: number; rowIndex: number }[] = [];
searchResults.forEach((result, titleIndex) => {
if (!unExpandedTitles[titleIndex] && searchText) {
Object.keys(result ?? {}).forEach(title => {
result[title].forEach((row: any, rowIndex: any) => {
selectedResults.push({ titleIndex, rowIndex });
});
});
}
});
setAllSelectResults(selectedResults);
}, [unExpandedTitles, searchText, searchResults]);

const preRow = useCallback((titleIndex: number, rowIndex: number) => {
const index = allSelectResults.findIndex(item => {
return item.titleIndex === titleIndex && item.rowIndex === rowIndex;
});
if (index - 1 >= 0) {
return allSelectResults[index - 1];
}
return {titleIndex, rowIndex};
}, [allSelectResults]);

const nextRow = useCallback((titleIndex: number, rowIndex: number) => {
const index = allSelectResults.findIndex(item => item.titleIndex === titleIndex && item.rowIndex === rowIndex);
if (allSelectResults.length > index + 1) {
return allSelectResults[index + 1];
}
return {titleIndex, rowIndex};
}, [allSelectResults]);

const handleKeyDown = useCallback((event: { metaKey: any; shiftKey: any; key: string; preventDefault: () => void; }) => {
if (event.key === 'ArrowDown') {
event.preventDefault();
setSelectedRow((pre) => nextRow(pre.titleIndex, pre.rowIndex));
} else if (event.key === 'ArrowUp') {
event.preventDefault();
setSelectedRow((pre) => preRow(pre.titleIndex, pre.rowIndex));
}
}, [selectedRow, allSelectResults]);

useEffect(() => {
const current = innerRef?.current as unknown as HTMLElement;
if (current) {
current.addEventListener('keydown', handleKeyDown);
return () => {
current.removeEventListener('keydown', handleKeyDown);
};
}
}, [innerRef, handleKeyDown]);

useEffect(() => {
if (selectedRow.titleIndex >= 0 && searchResults && searchResults.length > selectedRow.titleIndex) {
let keys = Object.keys(searchResults[selectedRow.titleIndex]);
if (keys && keys.length > 0) {
let title = keys[0];
let entry = searchResults[selectedRow.titleIndex][title][selectedRow.rowIndex];
onSelectedLine && onSelectedLine(title ,entry.line);
}
}
}, [selectedRow]);

const toggleExpand = (expanded: any, titleIndex: any) => {
setUnExpandedTitles((prev) => ({
...prev,
[titleIndex]: !expanded,
}));
setSelectedRow({ titleIndex: -1, rowIndex: -1 });
};

const handleRowSelection = (titleIndex: any, title: any, rowIndex: any, row: any) => {
setSelectedRow({ titleIndex, rowIndex });
};

return (
<div
ref={innerRef}
className="music-monaco-editor-list-wrapper"
style={{...style,
overflow: 'auto',
background: 'var(--monaco-editor-background)',
display: 'flex',
flexDirection: 'column',
}}
tabIndex={0}
>
<SearchInput
searchText={searchText}
setSearchText={setSearchText}
onClose={onClose}
/>
<SearchResult
searchResults={searchResults}
unExpandedTitles={unExpandedTitles}
searchText={searchText}
selectedRow={selectedRow}
handleRowSelection={handleRowSelection}
toggleExpand={toggleExpand}
/>
</div>
);
};

export default SearchAndReplace;
Loading