Skip to content

Commit

Permalink
feat: files path hash state (#28)
Browse files Browse the repository at this point in the history
> 优化 files 产物预览相关功能
* 🧶 支持路径,行号在 url 中映射,`/package/:name/files/:path#Ld-d` 便于分享
* 🚗 monaco loader 配置 npmmirror cdn


![image](https://github.com/cnpm/cnpmweb/assets/5574625/ab18424a-5899-49ef-808f-4f56fa013730)
  • Loading branch information
elrrrrrrr authored Aug 23, 2023
1 parent 14aaad0 commit bb9bb20
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@ant-design/cssinjs": "^1.11.1",
"@ant-design/icons": "^5.2.5",
"@gravatar/js": "^1.1.1",
"@monaco-editor/loader": "^1.3.3",
"@monaco-editor/react": "^4.4.2",
"@types/node": "20.4.1",
"@types/react": "18.2.14",
Expand Down
49 changes: 44 additions & 5 deletions src/components/CodeViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
'use client';
import Editor from '@monaco-editor/react';
import loader from '@monaco-editor/loader';
import { File, useFileContent } from '@/hooks/useFile';
import useHighlightHash, { parseHash } from '@/hooks/useHighlightHash';
import { useThemeMode } from 'antd-style';
import { useEffect, useRef } from 'react';

loader.config({
paths: {
vs: 'https://registry.npmmirror.com/monaco-editor/0.41.0/files/min/vs',
},
});

function highlightEditor(editor: any) {
const [start, end] = parseHash(window.location.hash?.replace('#', ''));
if (start !== null && window) {
editor.setSelection(new (window as any).monaco.Range(start, 1, end, 1));
}
}

export const CodeViewer = ({
selectedFile,
Expand All @@ -12,18 +28,36 @@ export const CodeViewer = ({
pkgName: string;
spec?: string;
}) => {
const editorRef = useRef<any>(null);
const { themeMode: theme } = useThemeMode();

const { data: code, isLoading } = useFileContent(
const { data: code } = useFileContent(
{ fullname: pkgName, spec },
selectedFile?.path || ''
);
let language = selectedFile?.path.split('.').pop();

if (language === 'js' || language === 'jsx') language = 'javascript';
let language = selectedFile?.path.split('.').pop();
if (
language === 'js' ||
language === 'jsx' ||
language === 'map'
)
language = 'javascript';
else if (language === 'ts' || language === 'tsx') language = 'typescript';
else if (language === 'md') language = 'markdown';

const [_, setRange] = useHighlightHash();

const handleEditorMouseDown = () => {
const { startLineNumber, endLineNumber } = editorRef.current.getSelection();

if (startLineNumber) {
setRange(startLineNumber, endLineNumber);
}
};

if (!selectedFile) return <></>;

if (!selectedFile) return null;

return (
<div
Expand All @@ -35,10 +69,15 @@ export const CodeViewer = ({
>
<Editor
height='100vh'
language={language}
value={code ? code : 'Loading...'}
language={language}
theme={`vs-${theme}`}
options={{ readOnly: true, fontSize: 16 }}
onMount={(editor) => {
editorRef.current = editor;
editor.onMouseUp(handleEditorMouseDown);
highlightEditor(editor);
}}
/>
</div>
);
Expand Down
1 change: 0 additions & 1 deletion src/components/FileTree/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ const FileDiv = ({file, icon, selectedFile, onClick}: {
display: 'flex',
alignItems: 'center',
paddingLeft: depth * 16,
// backgroundColor: isSelected ? "#eee" : "transparent",
cursor: 'pointer',
minWidth: 'max-content',
}}
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/useHighlightHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

export function parseHash(hash: string) {
const singleLineMatch = hash?.match(/^L(\d+)$/);
const multiLineMatch = hash?.match(/^L(\d+)-L(\d+)$/);
if (singleLineMatch) {
const line = parseInt(singleLineMatch[1], 10);
return [line, line];
} else if (multiLineMatch) {
const startLine = parseInt(multiLineMatch[1], 10);
const endLine = parseInt(multiLineMatch[2], 10);
return [startLine, endLine];
}
return [null, null];
}

export default function useHighlightHash() {
const router = useRouter();
const [highlightRange, setHighlightRange] = useState<any>([null, null]);

useEffect(() => {
const res = parseHash(router.asPath.split('#')[1]);
setHighlightRange(res);
}, [router.asPath]);

const setHashFromSelection = (start: number, end: number) => {
const { pathname, search } = window.location;

if (start === end) {
router.replace(`${pathname}${search}#L${start}`, undefined, { shallow: true });
} else {
router.replace(`${pathname}${search}#L${start}-L${end}`, undefined, { shallow: true });
}
};

return [highlightRange, setHashFromSelection];
}
31 changes: 31 additions & 0 deletions src/hooks/usePathState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';

export function usePathState(pattern: string): [string, (newValue: string) => void] {
const router = useRouter();
const regexPattern = useMemo(() => {
return new RegExp(pattern.replace('*', '(.*)'));
}, [pattern]);
const match = router.asPath.match(regexPattern);

const [state, setState] = useState(match ? match[1] : '');

useEffect(() => {
const handleRouteChange = (url: string) => {
const match = url.match(regexPattern);
setState(match ? match[1] : '');
};

router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router, regexPattern]);

const setPathState = (newValue: string) => {
const newPath = pattern.replace('*', newValue.replace(/^\//, ''));
router.replace(newPath, newPath, { shallow: true });
};

return [state?.split('#')[0], setPathState];
}
7 changes: 3 additions & 4 deletions src/pages/package/[...slug]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ThemeMode, ThemeProvider as _ThemeProvider } from 'antd-style';
import { ThemeProvider as _ThemeProvider } from 'antd-style';
import PageHome from '@/slugs/home'
import PageFiles from '@/slugs/files'
import PageVersions from '@/slugs/versions'
Expand Down Expand Up @@ -93,9 +93,8 @@ export default function PackagePage({
);
}

let type = router.asPath
.split('?')[0]
.replace(`/package/${resData.name}/`, '') as keyof typeof PageMap;
let type = router.query?.slug?.[1] as keyof typeof PageMap;

const version =
(router.query.version as string) || resData?.['dist-tags']?.latest;

Expand Down
13 changes: 9 additions & 4 deletions src/slugs/files/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import { CodeViewer } from "@/components/CodeViewer";
import { FileTree } from "@/components/FileTree";
import { Sidebar } from "@/components/Sidebar";
import { useDirs, File } from "@/hooks/useFile";
import { usePathState } from "@/hooks/usePathState";
import { PageProps } from "@/pages/package/[...slug]";
import { Spin } from "antd";
import { useState } from "react";

const Viewer = ({ manifest, version }: PageProps) => {
const [_selectedFile, setSelectedFile] = useState<File | undefined>();
const [path, setPath] = usePathState(`/package/${manifest.name}/files/*`);

const { data: rootDir, isLoading } = useDirs({
fullname: manifest.name,
spec: version || 'latest',
});

const selectedFile =
_selectedFile ||
rootDir?.files?.find((item: File) => item?.path === '/package.json');

const onSelect = (file: File) => setSelectedFile(file);
let selectedFile = _selectedFile || { path: `/${path || 'package.json'}`, type: 'file' };

const onSelect = (file: File) => {
setSelectedFile(file)
setPath(file.path);
};

if (isLoading) {
return (
Expand Down

0 comments on commit bb9bb20

Please sign in to comment.