mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 12:08:41 -09:00
search box overhaul
This commit is contained in:
parent
502cdbb3b2
commit
9ab2ba9f08
7 changed files with 351 additions and 93 deletions
|
|
@ -1,29 +0,0 @@
|
|||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<symbol id="left-arrow" viewBox="0 0 10 10">
|
||||
<title>left arrow</title>
|
||||
<g stroke-linecap="round">
|
||||
<line x1="0.5" y1="5" x2="8.5" y2="5" stroke="#000" />
|
||||
<line x1="0.5" y1="5" x2="3.5" y2="2" stroke="#000" />
|
||||
<line x1="0.5" y1="5" x2="3.5" y2="8" stroke="#000" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="right-arrow" viewBox="0 0 10 10">
|
||||
<title>right arrow</title>
|
||||
<g stroke-linecap="round">
|
||||
<line x1="1.5" y1="5" x2="9.5" y2="5" stroke="#000" />
|
||||
<line x1="9.5" y1="5" x2="6.5" y2="2" stroke="#000" />
|
||||
<line x1="9.5" y1="5" x2="6.5" y2="8" stroke="#000" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="cancel" viewBox="0 0 10 10">
|
||||
<title>cancel</title>
|
||||
<g stroke-linecap="round">
|
||||
<line x1="5" y1="5" x2="8" y2="8" stroke="#000" />
|
||||
<line x1="5" y1="5" x2="8" y2="2" stroke="#000" />
|
||||
<line x1="5" y1="5" x2="2" y2="2" stroke="#000" />
|
||||
<line x1="5" y1="5" x2="2" y2="8" stroke="#000" />
|
||||
</g>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,76 +1,254 @@
|
|||
import React from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {SearchBoxProps} from '../hyper';
|
||||
import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp';
|
||||
import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown';
|
||||
import {VscClose} from '@react-icons/all-files/vsc/VscClose';
|
||||
import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive';
|
||||
import {VscRegex} from '@react-icons/all-files/vsc/VscRegex';
|
||||
import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const searchBoxStyling: React.CSSProperties = {
|
||||
float: 'right',
|
||||
height: '28px',
|
||||
backgroundColor: 'white',
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '0px',
|
||||
width: '224px',
|
||||
zIndex: 9999
|
||||
type SearchButtonColors = {
|
||||
foregroundColor: string;
|
||||
selectionColor: string;
|
||||
backgroundColor: string;
|
||||
};
|
||||
|
||||
const enterKey = 13;
|
||||
type SearchButtonProps = React.PropsWithChildren<
|
||||
{
|
||||
onClick: () => void;
|
||||
active: boolean;
|
||||
title: string;
|
||||
} & SearchButtonColors
|
||||
>;
|
||||
|
||||
export default class SearchBox extends React.PureComponent<SearchBoxProps> {
|
||||
const SearchButton = ({
|
||||
onClick,
|
||||
active,
|
||||
title,
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
selectionColor,
|
||||
children
|
||||
}: SearchButtonProps) => {
|
||||
const handleKeyUp = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
onClick();
|
||||
}
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={clsx('search-button', {'search-button-active': active})}
|
||||
tabIndex={0}
|
||||
onKeyUp={handleKeyUp}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
<style jsx>
|
||||
{`
|
||||
.search-button {
|
||||
cursor: pointer;
|
||||
color: ${foregroundColor};
|
||||
padding: 2px;
|
||||
margin: 4px 0px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-button:focus {
|
||||
outline: ${selectionColor} solid 2px;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
|
||||
.search-button-active {
|
||||
background-color: ${selectionColor};
|
||||
}
|
||||
|
||||
.search-button-active:hover {
|
||||
background-color: ${selectionColor};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class SearchBox extends React.PureComponent<SearchBoxProps> {
|
||||
searchTerm: string;
|
||||
input: HTMLInputElement | null = null;
|
||||
searchButtonColors: SearchButtonColors;
|
||||
|
||||
constructor(props: SearchBoxProps) {
|
||||
super(props);
|
||||
this.searchTerm = '';
|
||||
this.searchButtonColors = {
|
||||
backgroundColor: this.props.borderColor,
|
||||
selectionColor: this.props.selectionColor,
|
||||
foregroundColor: this.props.foregroundColor
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
this.searchTerm = event.currentTarget.value;
|
||||
if (event.keyCode === enterKey) {
|
||||
this.props.search(event.currentTarget.value);
|
||||
if (event.shiftKey && event.key === 'Enter') {
|
||||
this.props.prev(this.searchTerm);
|
||||
} else if (event.key === 'Enter') {
|
||||
this.props.next(this.searchTerm);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
this.input?.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
caseSensitive,
|
||||
wholeWord,
|
||||
regex,
|
||||
results,
|
||||
toggleCaseSensitive,
|
||||
toggleWholeWord,
|
||||
toggleRegex,
|
||||
next,
|
||||
prev,
|
||||
close,
|
||||
backgroundColor,
|
||||
foregroundColor,
|
||||
borderColor,
|
||||
selectionColor,
|
||||
font
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div style={searchBoxStyling}>
|
||||
<input type="text" className="search-box" onKeyUp={this.handleChange} ref={(input) => input?.focus()} />
|
||||
<svg className="search-button" onClick={() => this.props.prev(this.searchTerm)}>
|
||||
<use xlinkHref="./renderer/assets/search-icons.svg#left-arrow" />
|
||||
</svg>
|
||||
<svg className="search-button" onClick={() => this.props.next(this.searchTerm)}>
|
||||
<use xlinkHref="./renderer/assets/search-icons.svg#right-arrow" />
|
||||
</svg>
|
||||
<svg className="search-button" onClick={() => this.props.close()}>
|
||||
<use xlinkHref="./renderer/assets/search-icons.svg#cancel" />
|
||||
</svg>
|
||||
<div className="flex-row search-container">
|
||||
<div className="flex-row search-box">
|
||||
<input
|
||||
className="search-input"
|
||||
type="text"
|
||||
onKeyDown={this.handleChange}
|
||||
ref={(input) => {
|
||||
this.input = input;
|
||||
}}
|
||||
placeholder="Search"
|
||||
></input>
|
||||
|
||||
<SearchButton
|
||||
onClick={toggleCaseSensitive}
|
||||
active={caseSensitive}
|
||||
title="Match Case"
|
||||
{...this.searchButtonColors}
|
||||
>
|
||||
<VscCaseSensitive size="14px" />
|
||||
</SearchButton>
|
||||
|
||||
<SearchButton
|
||||
onClick={toggleWholeWord}
|
||||
active={wholeWord}
|
||||
title="Match Whole Word"
|
||||
{...this.searchButtonColors}
|
||||
>
|
||||
<VscWholeWord size="14px" />
|
||||
</SearchButton>
|
||||
|
||||
<SearchButton
|
||||
onClick={toggleRegex}
|
||||
active={regex}
|
||||
title="Use Regular Expression"
|
||||
{...this.searchButtonColors}
|
||||
>
|
||||
<VscRegex size="14px" />
|
||||
</SearchButton>
|
||||
</div>
|
||||
|
||||
<span style={{minWidth: '60px', marginLeft: '4px'}}>
|
||||
{results === undefined
|
||||
? ''
|
||||
: results.resultCount === 0
|
||||
? 'No results'
|
||||
: `${results.resultIndex + 1} of ${results.resultCount}`}
|
||||
</span>
|
||||
|
||||
<div className="flex-row">
|
||||
<SearchButton
|
||||
onClick={() => prev(this.searchTerm)}
|
||||
active={false}
|
||||
title="Previous Match"
|
||||
{...this.searchButtonColors}
|
||||
>
|
||||
<VscArrowUp size="14px" />
|
||||
</SearchButton>
|
||||
|
||||
<SearchButton
|
||||
onClick={() => next(this.searchTerm)}
|
||||
active={false}
|
||||
title="Next Match"
|
||||
{...this.searchButtonColors}
|
||||
>
|
||||
<VscArrowDown size="14px" />
|
||||
</SearchButton>
|
||||
|
||||
<SearchButton onClick={() => close()} active={false} title="Close" {...this.searchButtonColors}>
|
||||
<VscClose size="14px" />
|
||||
</SearchButton>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.search-box {
|
||||
font-size: 18px;
|
||||
padding: 3px 6px;
|
||||
width: 152px;
|
||||
border: none;
|
||||
float: left;
|
||||
.search-container {
|
||||
background-color: ${backgroundColor};
|
||||
border: 1px solid ${borderColor};
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 4px;
|
||||
z-index: 10;
|
||||
padding: 4px;
|
||||
font-family: ${font};
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
.search-input {
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${foregroundColor};
|
||||
align-self: stretch;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background-color: #ffffff;
|
||||
color: black;
|
||||
padding: 7px 5.5px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
transition-duration: 0.4s;
|
||||
cursor: pointer;
|
||||
height: 27px;
|
||||
width: 24px;
|
||||
float: left;
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.search-button:hover {
|
||||
background-color: #e7e7e7;
|
||||
|
||||
.search-box {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
outline: ${borderColor} solid 1px;
|
||||
background-color: ${backgroundColor};
|
||||
color: ${foregroundColor};
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: ${foregroundColor};
|
||||
}
|
||||
|
||||
.search-box:focus-within {
|
||||
outline: ${selectionColor} solid 2px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
|
@ -78,3 +256,5 @@ export default class SearchBox extends React.PureComponent<SearchBoxProps> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchBox;
|
||||
|
|
|
|||
|
|
@ -137,6 +137,15 @@ export default class StyleSheet extends React.PureComponent<StyleSheetProps> {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm .xterm-decoration-overview-ruler {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
right: 0px;
|
||||
top: unset;
|
||||
left: unset;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import {Terminal, ITerminalOptions, IDisposable} from 'xterm';
|
||||
import {FitAddon} from 'xterm-addon-fit';
|
||||
import {WebLinksAddon} from 'xterm-addon-web-links';
|
||||
import {SearchAddon} from 'xterm-addon-search';
|
||||
import {SearchAddon, ISearchDecorationOptions} from 'xterm-addon-search';
|
||||
import {WebglAddon} from 'xterm-addon-webgl';
|
||||
import {LigaturesAddon} from 'xterm-addon-ligatures';
|
||||
import {Unicode11Addon} from 'xterm-addon-unicode11';
|
||||
|
|
@ -10,9 +10,12 @@ import {clipboard, shell} from 'electron';
|
|||
import Color from 'color';
|
||||
import terms from '../terms';
|
||||
import processClipboard from '../utils/paste';
|
||||
import SearchBox from './searchBox';
|
||||
import _SearchBox from './searchBox';
|
||||
import {TermProps} from '../hyper';
|
||||
import {ObjectTypedKeys} from '../utils/object';
|
||||
import {decorate} from '../utils/plugins';
|
||||
|
||||
const SearchBox = decorate(_SearchBox, 'SearchBox');
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform);
|
||||
|
||||
|
|
@ -78,11 +81,27 @@ const getTermOptions = (props: TermProps): ITerminalOptions => {
|
|||
brightCyan: props.colors.lightCyan,
|
||||
brightWhite: props.colors.lightWhite
|
||||
},
|
||||
screenReaderMode: props.screenReaderMode
|
||||
screenReaderMode: props.screenReaderMode,
|
||||
overviewRulerWidth: 20
|
||||
};
|
||||
};
|
||||
|
||||
export default class Term extends React.PureComponent<TermProps> {
|
||||
export default class Term extends React.PureComponent<
|
||||
TermProps,
|
||||
{
|
||||
searchOptions: {
|
||||
caseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
regex: boolean;
|
||||
};
|
||||
searchResults:
|
||||
| {
|
||||
resultIndex: number;
|
||||
resultCount: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
> {
|
||||
termRef: HTMLElement | null;
|
||||
termWrapperRef: HTMLElement | null;
|
||||
termOptions: ITerminalOptions;
|
||||
|
|
@ -94,6 +113,16 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
term!: Terminal;
|
||||
resizeObserver!: ResizeObserver;
|
||||
resizeTimeout!: NodeJS.Timeout;
|
||||
searchDecorations: ISearchDecorationOptions;
|
||||
state = {
|
||||
searchOptions: {
|
||||
caseSensitive: false,
|
||||
wholeWord: false,
|
||||
regex: false
|
||||
},
|
||||
searchResults: undefined
|
||||
};
|
||||
|
||||
constructor(props: TermProps) {
|
||||
super(props);
|
||||
props.ref_(props.uid, this);
|
||||
|
|
@ -104,6 +133,11 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
this.termDefaultBellSound = null;
|
||||
this.fitAddon = new FitAddon();
|
||||
this.searchAddon = new SearchAddon();
|
||||
this.searchDecorations = {
|
||||
activeMatchColorOverviewRuler: Color(this.props.cursorColor).hex(),
|
||||
matchOverviewRuler: Color(this.props.borderColor).hex(),
|
||||
activeMatchBackground: Color(this.props.cursorColor).hex()
|
||||
};
|
||||
}
|
||||
|
||||
// The main process shows this in the About dialog
|
||||
|
|
@ -242,6 +276,15 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
);
|
||||
}
|
||||
|
||||
this.disposableListeners.push(
|
||||
this.searchAddon.onDidChangeResults((results) => {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
searchResults: results
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
window.addEventListener('paste', this.onWindowPaste, {
|
||||
capture: true
|
||||
});
|
||||
|
|
@ -302,20 +345,28 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
this.term.reset();
|
||||
}
|
||||
|
||||
search = (searchTerm = '') => {
|
||||
this.searchAddon.findNext(searchTerm);
|
||||
};
|
||||
|
||||
searchNext = (searchTerm: string) => {
|
||||
this.searchAddon.findNext(searchTerm);
|
||||
this.searchAddon.findNext(searchTerm, {
|
||||
...this.state.searchOptions,
|
||||
decorations: this.searchDecorations
|
||||
});
|
||||
};
|
||||
|
||||
searchPrevious = (searchTerm: string) => {
|
||||
this.searchAddon.findPrevious(searchTerm);
|
||||
this.searchAddon.findPrevious(searchTerm, {
|
||||
...this.state.searchOptions,
|
||||
decorations: this.searchDecorations
|
||||
});
|
||||
};
|
||||
|
||||
closeSearchBox = () => {
|
||||
this.props.onCloseSearch();
|
||||
this.searchAddon.clearDecorations();
|
||||
this.searchAddon.clearActiveDecoration();
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
searchResults: undefined
|
||||
}));
|
||||
this.term.focus();
|
||||
};
|
||||
|
||||
|
|
@ -350,8 +401,8 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
// otherwise use the default sound found in xterm.
|
||||
nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound!;
|
||||
|
||||
if (!prevProps.search && this.props.search) {
|
||||
this.search();
|
||||
if (prevProps.search && !this.props.search) {
|
||||
this.closeSearchBox();
|
||||
}
|
||||
|
||||
// Update only options that have changed.
|
||||
|
|
@ -445,14 +496,38 @@ export default class Term extends React.PureComponent<TermProps> {
|
|||
{this.props.customChildren}
|
||||
{this.props.search ? (
|
||||
<SearchBox
|
||||
search={this.search}
|
||||
next={this.searchNext}
|
||||
prev={this.searchPrevious}
|
||||
close={this.closeSearchBox}
|
||||
caseSensitive={this.state.searchOptions.caseSensitive}
|
||||
wholeWord={this.state.searchOptions.wholeWord}
|
||||
regex={this.state.searchOptions.regex}
|
||||
results={this.state.searchResults}
|
||||
toggleCaseSensitive={() =>
|
||||
this.setState({
|
||||
...this.state,
|
||||
searchOptions: {...this.state.searchOptions, caseSensitive: !this.state.searchOptions.caseSensitive}
|
||||
})
|
||||
}
|
||||
toggleWholeWord={() =>
|
||||
this.setState({
|
||||
...this.state,
|
||||
searchOptions: {...this.state.searchOptions, wholeWord: !this.state.searchOptions.wholeWord}
|
||||
})
|
||||
}
|
||||
toggleRegex={() =>
|
||||
this.setState({
|
||||
...this.state,
|
||||
searchOptions: {...this.state.searchOptions, regex: !this.state.searchOptions.regex}
|
||||
})
|
||||
}
|
||||
selectionColor={this.props.selectionColor}
|
||||
backgroundColor={this.props.backgroundColor}
|
||||
foregroundColor={this.props.foregroundColor}
|
||||
borderColor={this.props.borderColor}
|
||||
font={this.props.uiFontFamily}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<style jsx global>{`
|
||||
.term_fit {
|
||||
|
|
|
|||
13
lib/hyper.d.ts
vendored
13
lib/hyper.d.ts
vendored
|
|
@ -314,10 +314,21 @@ import {TermGroupConnectedProps} from './components/term-group';
|
|||
export type TermGroupProps = TermGroupConnectedProps & TermGroupOwnProps;
|
||||
|
||||
export type SearchBoxProps = {
|
||||
search: (searchTerm: string) => void;
|
||||
caseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
regex: boolean;
|
||||
results: {resultIndex: number; resultCount: number} | undefined;
|
||||
toggleCaseSensitive: () => void;
|
||||
toggleWholeWord: () => void;
|
||||
toggleRegex: () => void;
|
||||
next: (searchTerm: string) => void;
|
||||
prev: (searchTerm: string) => void;
|
||||
close: () => void;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
borderColor: string;
|
||||
selectionColor: string;
|
||||
font: string;
|
||||
};
|
||||
|
||||
import {FitAddon} from 'xterm-addon-fit';
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.0.9",
|
||||
"@react-icons/all-files": "4.1.0",
|
||||
"args": "5.0.3",
|
||||
"chalk": "5.2.0",
|
||||
"clsx": "1.2.1",
|
||||
"color": "4.2.3",
|
||||
"columnify": "1.6.0",
|
||||
"css-loader": "6.7.3",
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -797,6 +797,11 @@
|
|||
tiny-glob "^0.2.9"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@react-icons/all-files@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz#477284873a0821928224b6fc84c62d2534d6650b"
|
||||
integrity sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
|
|
@ -2411,6 +2416,11 @@ clone@^1.0.2:
|
|||
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
||||
|
||||
clsx@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
code-excerpt@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz#2de7d46e98514385cb01f7b3b741320115f4c95e"
|
||||
|
|
|
|||
Loading…
Reference in a new issue