@parca/profile 0.16.347 → 0.16.349
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/SourceView/Highlighter.d.ts +2 -1
- package/dist/SourceView/Highlighter.js +19 -6
- package/dist/SourceView/LineNo.d.ts +2 -2
- package/dist/SourceView/LineNo.js +2 -2
- package/dist/SourceView/index.js +36 -4
- package/dist/SourceView/useSelectedLineRange.d.ts +7 -0
- package/dist/SourceView/useSelectedLineRange.js +29 -0
- package/package.json +3 -3
- package/src/SourceView/Highlighter.tsx +21 -6
- package/src/SourceView/LineNo.tsx +4 -4
- package/src/SourceView/index.tsx +50 -3
- package/src/SourceView/useSelectedLineRange.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.16.349](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.348...@parca/profile@0.16.349) (2024-02-27)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.16.348](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.347...@parca/profile@0.16.348) (2024-02-27)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
6
14
|
## [0.16.347](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.346...@parca/profile@0.16.347) (2024-02-22)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MouseEventHandler } from 'react';
|
|
1
2
|
import { Vector } from 'apache-arrow';
|
|
2
3
|
import { type createElementProps } from 'react-syntax-highlighter';
|
|
3
4
|
interface RendererProps {
|
|
@@ -11,6 +12,6 @@ interface HighlighterProps {
|
|
|
11
12
|
content: string;
|
|
12
13
|
renderer?: Renderer;
|
|
13
14
|
}
|
|
14
|
-
export declare const profileAwareRenderer: (cumulative: Vector | null, flat: Vector | null, total: bigint, filtered: bigint) => Renderer;
|
|
15
|
+
export declare const profileAwareRenderer: (cumulative: Vector | null, flat: Vector | null, total: bigint, filtered: bigint, onContextMenu: MouseEventHandler<HTMLDivElement>) => Renderer;
|
|
15
16
|
export declare const Highlighter: ({ file, content, renderer }: HighlighterProps) => JSX.Element;
|
|
16
17
|
export {};
|
|
@@ -17,10 +17,11 @@ import { scaleLinear } from 'd3-scale';
|
|
|
17
17
|
import SyntaxHighlighter, { createElement } from 'react-syntax-highlighter';
|
|
18
18
|
import { atomOneDark, atomOneLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
19
19
|
import { Tooltip } from 'react-tooltip';
|
|
20
|
-
import { useParcaContext
|
|
20
|
+
import { useParcaContext } from '@parca/components';
|
|
21
21
|
import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
|
|
22
22
|
import { LineNo } from './LineNo';
|
|
23
23
|
import { langaugeFromFile } from './lang-detector';
|
|
24
|
+
import useLineRange from './useSelectedLineRange';
|
|
24
25
|
// cannot make this a function on the number as we need the classes to be static for tailwind
|
|
25
26
|
const charsToWidthMap = {
|
|
26
27
|
1: 'w-3',
|
|
@@ -55,17 +56,29 @@ const LineProfileMetadata = ({ value, total, filtered, }) => {
|
|
|
55
56
|
const charsToWidth = (chars) => {
|
|
56
57
|
return charsToWidthMap[chars];
|
|
57
58
|
};
|
|
58
|
-
export const profileAwareRenderer = (cumulative, flat, total, filtered) => {
|
|
59
|
+
export const profileAwareRenderer = (cumulative, flat, total, filtered, onContextMenu) => {
|
|
59
60
|
return function ProfileAwareRenderer({ rows, stylesheet, useInlineStyles, }) {
|
|
60
61
|
const lineNumberWidth = charsToWidth(rows.length.toString().length);
|
|
61
|
-
const
|
|
62
|
+
const { startLine, endLine, setLineRange } = useLineRange();
|
|
62
63
|
return (_jsx(_Fragment, { children: rows.map((node, i) => {
|
|
63
64
|
const lineNumber = node.children[0].children[0].value;
|
|
64
|
-
const isCurrentLine =
|
|
65
|
+
const isCurrentLine = lineNumber >= startLine && lineNumber <= endLine;
|
|
65
66
|
node.children = node.children.slice(1);
|
|
66
|
-
return (_jsxs("div", { className: "flex gap-1", children: [_jsx("div", { className: cx('shrink-0 overflow-hidden border-r border-r-gray-200 text-right dark:border-r-gray-700', lineNumberWidth), children: _jsx(LineNo, { value: lineNumber, isCurrent: isCurrentLine,
|
|
67
|
+
return (_jsxs("div", { className: "flex gap-1", children: [_jsx("div", { className: cx('shrink-0 overflow-hidden border-r border-r-gray-200 text-right dark:border-r-gray-700', lineNumberWidth), children: _jsx(LineNo, { value: lineNumber, isCurrent: isCurrentLine, selectLine: (isShiftDown = false) => {
|
|
68
|
+
if (!isShiftDown) {
|
|
69
|
+
setLineRange(lineNumber, lineNumber);
|
|
70
|
+
}
|
|
71
|
+
if (isShiftDown && startLine != null) {
|
|
72
|
+
if (startLine > lineNumber) {
|
|
73
|
+
setLineRange(lineNumber, startLine);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
setLineRange(startLine, lineNumber);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} }) }), _jsx(LineProfileMetadata, { value: cumulative?.get(i) ?? 0n, total: total, filtered: filtered }), _jsx(LineProfileMetadata, { value: flat?.get(i) ?? 0n, total: total, filtered: filtered }), _jsx("div", { className: cx('w-full flex-grow-0 border-l border-gray-200 pl-1 dark:border-gray-700', {
|
|
67
80
|
'bg-yellow-200 dark:bg-yellow-700': isCurrentLine,
|
|
68
|
-
}), children: createElement({
|
|
81
|
+
}), onContextMenu: onContextMenu, children: createElement({
|
|
69
82
|
key: `source-line-${i}`,
|
|
70
83
|
node,
|
|
71
84
|
stylesheet,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
value: number;
|
|
3
3
|
isCurrent?: boolean;
|
|
4
|
-
|
|
4
|
+
selectLine?: (isShiftDown?: boolean) => void;
|
|
5
5
|
}
|
|
6
|
-
export declare const LineNo: ({ value, isCurrent,
|
|
6
|
+
export declare const LineNo: ({ value, isCurrent, selectLine }: Props) => JSX.Element;
|
|
7
7
|
export {};
|
|
@@ -13,7 +13,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
import { useEffect, useRef } from 'react';
|
|
15
15
|
import cx from 'classnames';
|
|
16
|
-
export const LineNo = ({ value, isCurrent = false,
|
|
16
|
+
export const LineNo = ({ value, isCurrent = false, selectLine }) => {
|
|
17
17
|
const ref = useRef(null);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (isCurrent && ref.current !== null) {
|
|
@@ -26,7 +26,7 @@ export const LineNo = ({ value, isCurrent = false, setCurrentLine }) => {
|
|
|
26
26
|
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
27
27
|
}
|
|
28
28
|
}, [isCurrent]);
|
|
29
|
-
return (_jsx("code", { ref: ref, onClick:
|
|
29
|
+
return (_jsx("code", { ref: ref, onClick: e => typeof selectLine === 'function' && selectLine(e.shiftKey), className: cx('cursor-pointer px-1 select-none', {
|
|
30
30
|
'border-l border-l-amber-900 bg-yellow-200 dark:bg-yellow-700': isCurrent,
|
|
31
31
|
}), children: value.toString() + '\n' }));
|
|
32
32
|
};
|
package/dist/SourceView/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Copyright 2022 The Parca Authors
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
// you may not use this file except in compliance with the License.
|
|
@@ -11,16 +11,42 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
import React, { useEffect } from 'react';
|
|
14
|
+
import React, { useEffect, useMemo } from 'react';
|
|
15
15
|
import { tableFromIPC } from 'apache-arrow';
|
|
16
16
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
17
|
+
import { Item, Menu, useContextMenu } from 'react-contexify';
|
|
17
18
|
import { SourceSkeleton, useParcaContext, useURLState } from '@parca/components';
|
|
18
19
|
import { ExpandOnHover } from '../GraphTooltipArrow/ExpandOnHoverValue';
|
|
19
20
|
import { truncateStringReverse } from '../utils';
|
|
20
21
|
import { Highlighter, profileAwareRenderer } from './Highlighter';
|
|
22
|
+
import useLineRange from './useSelectedLineRange';
|
|
23
|
+
const MENU_ID = 'source-view-context-menu';
|
|
21
24
|
export const SourceView = React.memo(function SourceView({ data, loading, total, filtered, setActionButtons, }) {
|
|
22
25
|
const [sourceFileName] = useURLState({ param: 'source_filename', navigateTo: () => { } });
|
|
23
|
-
const { isDarkMode } = useParcaContext();
|
|
26
|
+
const { isDarkMode, sourceViewContextMenuItems = [] } = useParcaContext();
|
|
27
|
+
const sourceCode = useMemo(() => {
|
|
28
|
+
if (data === undefined) {
|
|
29
|
+
return [''];
|
|
30
|
+
}
|
|
31
|
+
return data.source.split('\n');
|
|
32
|
+
}, [data]);
|
|
33
|
+
const { show } = useContextMenu({
|
|
34
|
+
id: MENU_ID,
|
|
35
|
+
});
|
|
36
|
+
const { startLine, endLine } = useLineRange();
|
|
37
|
+
const selectedCode = useMemo(() => {
|
|
38
|
+
if (startLine === -1 && endLine === -1) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
if (startLine === endLine) {
|
|
42
|
+
return sourceCode[startLine - 1];
|
|
43
|
+
}
|
|
44
|
+
let code = '';
|
|
45
|
+
for (let i = startLine - 1; i < endLine; i++) {
|
|
46
|
+
code += sourceCode[i] + '\n';
|
|
47
|
+
}
|
|
48
|
+
return code;
|
|
49
|
+
}, [startLine, endLine, sourceCode]);
|
|
24
50
|
useEffect(() => {
|
|
25
51
|
setActionButtons?.(_jsx("div", { className: "px-2", children: _jsx(ExpandOnHover, { value: sourceFileName, displayValue: truncateStringReverse(sourceFileName, 50) }) }));
|
|
26
52
|
}, [sourceFileName, setActionButtons]);
|
|
@@ -30,9 +56,15 @@ export const SourceView = React.memo(function SourceView({ data, loading, total,
|
|
|
30
56
|
if (data === undefined) {
|
|
31
57
|
return _jsx(_Fragment, { children: "Source code not uploaded for this build." });
|
|
32
58
|
}
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const onContextMenu = (event) => {
|
|
61
|
+
show({
|
|
62
|
+
event,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
33
65
|
const table = tableFromIPC(data.record);
|
|
34
66
|
const cumulative = table.getChild('cumulative');
|
|
35
67
|
const flat = table.getChild('flat');
|
|
36
|
-
return (_jsx(AnimatePresence, { children:
|
|
68
|
+
return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: [_jsx(Highlighter, { file: sourceFileName, content: data.source, renderer: profileAwareRenderer(cumulative, flat, total, filtered, onContextMenu) }), sourceViewContextMenuItems.length > 0 ? (_jsx(Menu, { id: MENU_ID, children: sourceViewContextMenuItems.map(item => (_jsx(Item, { onClick: () => item.action(selectedCode), children: item.label }, item.id))) })) : null] }, "source-view-loaded") }));
|
|
37
69
|
});
|
|
38
70
|
export default SourceView;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { useMemo } from 'react';
|
|
14
|
+
import { useURLState } from '@parca/components';
|
|
15
|
+
const useLineRange = () => {
|
|
16
|
+
const [sourceLine, setSourceLine] = useURLState({ param: 'source_line', navigateTo: () => { } });
|
|
17
|
+
const [startLine, endLine] = useMemo(() => {
|
|
18
|
+
if (sourceLine == null) {
|
|
19
|
+
return [-1, -1];
|
|
20
|
+
}
|
|
21
|
+
const [start, end] = sourceLine.split('-');
|
|
22
|
+
return [parseInt(start, 10), parseInt(end, 10)];
|
|
23
|
+
}, [sourceLine]);
|
|
24
|
+
const setLineRange = (start, end) => {
|
|
25
|
+
setSourceLine(`${start}-${end}`);
|
|
26
|
+
};
|
|
27
|
+
return { startLine, endLine, setLineRange };
|
|
28
|
+
};
|
|
29
|
+
export default useLineRange;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.349",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@parca/client": "^0.16.105",
|
|
7
|
-
"@parca/components": "^0.16.
|
|
7
|
+
"@parca/components": "^0.16.257",
|
|
8
8
|
"@parca/dynamicsize": "^0.16.60",
|
|
9
9
|
"@parca/hooks": "^0.0.43",
|
|
10
10
|
"@parca/parser": "^0.16.68",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"access": "public",
|
|
51
51
|
"registry": "https://registry.npmjs.org/"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "b672a3fdb3331c6111ebc303d65a286af2074f97"
|
|
54
54
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useId, useMemo} from 'react';
|
|
14
|
+
import {MouseEventHandler, useId, useMemo} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Vector} from 'apache-arrow';
|
|
17
17
|
import cx from 'classnames';
|
|
@@ -20,11 +20,12 @@ import SyntaxHighlighter, {createElement, type createElementProps} from 'react-s
|
|
|
20
20
|
import {atomOneDark, atomOneLight} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
21
21
|
import {Tooltip} from 'react-tooltip';
|
|
22
22
|
|
|
23
|
-
import {useParcaContext
|
|
23
|
+
import {useParcaContext} from '@parca/components';
|
|
24
24
|
|
|
25
25
|
import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
|
|
26
26
|
import {LineNo} from './LineNo';
|
|
27
27
|
import {langaugeFromFile} from './lang-detector';
|
|
28
|
+
import useLineRange from './useSelectedLineRange';
|
|
28
29
|
|
|
29
30
|
interface RendererProps {
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -110,7 +111,8 @@ export const profileAwareRenderer = (
|
|
|
110
111
|
cumulative: Vector | null,
|
|
111
112
|
flat: Vector | null,
|
|
112
113
|
total: bigint,
|
|
113
|
-
filtered: bigint
|
|
114
|
+
filtered: bigint,
|
|
115
|
+
onContextMenu: MouseEventHandler<HTMLDivElement>
|
|
114
116
|
): Renderer => {
|
|
115
117
|
return function ProfileAwareRenderer({
|
|
116
118
|
rows,
|
|
@@ -118,12 +120,13 @@ export const profileAwareRenderer = (
|
|
|
118
120
|
useInlineStyles,
|
|
119
121
|
}: RendererProps): JSX.Element {
|
|
120
122
|
const lineNumberWidth = charsToWidth(rows.length.toString().length);
|
|
121
|
-
const
|
|
123
|
+
const {startLine, endLine, setLineRange} = useLineRange();
|
|
124
|
+
|
|
122
125
|
return (
|
|
123
126
|
<>
|
|
124
127
|
{rows.map((node, i) => {
|
|
125
128
|
const lineNumber: number = node.children[0].children[0].value as number;
|
|
126
|
-
const isCurrentLine =
|
|
129
|
+
const isCurrentLine = lineNumber >= startLine && lineNumber <= endLine;
|
|
127
130
|
node.children = node.children.slice(1);
|
|
128
131
|
return (
|
|
129
132
|
<div className="flex gap-1" key={`${i}`}>
|
|
@@ -136,7 +139,18 @@ export const profileAwareRenderer = (
|
|
|
136
139
|
<LineNo
|
|
137
140
|
value={lineNumber}
|
|
138
141
|
isCurrent={isCurrentLine}
|
|
139
|
-
|
|
142
|
+
selectLine={(isShiftDown = false) => {
|
|
143
|
+
if (!isShiftDown) {
|
|
144
|
+
setLineRange(lineNumber, lineNumber);
|
|
145
|
+
}
|
|
146
|
+
if (isShiftDown && startLine != null) {
|
|
147
|
+
if (startLine > lineNumber) {
|
|
148
|
+
setLineRange(lineNumber, startLine);
|
|
149
|
+
} else {
|
|
150
|
+
setLineRange(startLine, lineNumber);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}}
|
|
140
154
|
/>
|
|
141
155
|
</div>
|
|
142
156
|
<LineProfileMetadata
|
|
@@ -152,6 +166,7 @@ export const profileAwareRenderer = (
|
|
|
152
166
|
'bg-yellow-200 dark:bg-yellow-700': isCurrentLine,
|
|
153
167
|
}
|
|
154
168
|
)}
|
|
169
|
+
onContextMenu={onContextMenu}
|
|
155
170
|
>
|
|
156
171
|
{createElement({
|
|
157
172
|
key: `source-line-${i}`,
|
|
@@ -18,10 +18,10 @@ import cx from 'classnames';
|
|
|
18
18
|
interface Props {
|
|
19
19
|
value: number;
|
|
20
20
|
isCurrent?: boolean;
|
|
21
|
-
|
|
21
|
+
selectLine?: (isShiftDown?: boolean) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const LineNo = ({value, isCurrent = false,
|
|
24
|
+
export const LineNo = ({value, isCurrent = false, selectLine}: Props): JSX.Element => {
|
|
25
25
|
const ref = useRef<HTMLDivElement>(null);
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
@@ -41,8 +41,8 @@ export const LineNo = ({value, isCurrent = false, setCurrentLine}: Props): JSX.E
|
|
|
41
41
|
return (
|
|
42
42
|
<code
|
|
43
43
|
ref={ref}
|
|
44
|
-
onClick={
|
|
45
|
-
className={cx('cursor-pointer px-1', {
|
|
44
|
+
onClick={e => typeof selectLine === 'function' && selectLine(e.shiftKey)}
|
|
45
|
+
className={cx('cursor-pointer px-1 select-none', {
|
|
46
46
|
'border-l border-l-amber-900 bg-yellow-200 dark:bg-yellow-700': isCurrent,
|
|
47
47
|
})}
|
|
48
48
|
>
|
package/src/SourceView/index.tsx
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {useEffect} from 'react';
|
|
14
|
+
import React, {useEffect, useMemo} from 'react';
|
|
15
15
|
|
|
16
16
|
import {tableFromIPC} from 'apache-arrow';
|
|
17
17
|
import {AnimatePresence, motion} from 'framer-motion';
|
|
18
|
+
import {Item, Menu, useContextMenu} from 'react-contexify';
|
|
18
19
|
|
|
19
20
|
import {Source} from '@parca/client';
|
|
20
21
|
import {SourceSkeleton, useParcaContext, useURLState} from '@parca/components';
|
|
@@ -22,6 +23,7 @@ import {SourceSkeleton, useParcaContext, useURLState} from '@parca/components';
|
|
|
22
23
|
import {ExpandOnHover} from '../GraphTooltipArrow/ExpandOnHoverValue';
|
|
23
24
|
import {truncateStringReverse} from '../utils';
|
|
24
25
|
import {Highlighter, profileAwareRenderer} from './Highlighter';
|
|
26
|
+
import useLineRange from './useSelectedLineRange';
|
|
25
27
|
|
|
26
28
|
interface SourceViewProps {
|
|
27
29
|
loading: boolean;
|
|
@@ -31,6 +33,8 @@ interface SourceViewProps {
|
|
|
31
33
|
setActionButtons?: (buttons: JSX.Element) => void;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const MENU_ID = 'source-view-context-menu';
|
|
37
|
+
|
|
34
38
|
export const SourceView = React.memo(function SourceView({
|
|
35
39
|
data,
|
|
36
40
|
loading,
|
|
@@ -39,7 +43,34 @@ export const SourceView = React.memo(function SourceView({
|
|
|
39
43
|
setActionButtons,
|
|
40
44
|
}: SourceViewProps): JSX.Element {
|
|
41
45
|
const [sourceFileName] = useURLState({param: 'source_filename', navigateTo: () => {}});
|
|
42
|
-
const {isDarkMode} = useParcaContext();
|
|
46
|
+
const {isDarkMode, sourceViewContextMenuItems = []} = useParcaContext();
|
|
47
|
+
|
|
48
|
+
const sourceCode = useMemo(() => {
|
|
49
|
+
if (data === undefined) {
|
|
50
|
+
return [''];
|
|
51
|
+
}
|
|
52
|
+
return data.source.split('\n');
|
|
53
|
+
}, [data]);
|
|
54
|
+
|
|
55
|
+
const {show} = useContextMenu({
|
|
56
|
+
id: MENU_ID,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const {startLine, endLine} = useLineRange();
|
|
60
|
+
|
|
61
|
+
const selectedCode = useMemo(() => {
|
|
62
|
+
if (startLine === -1 && endLine === -1) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
if (startLine === endLine) {
|
|
66
|
+
return sourceCode[startLine - 1];
|
|
67
|
+
}
|
|
68
|
+
let code = '';
|
|
69
|
+
for (let i = startLine - 1; i < endLine; i++) {
|
|
70
|
+
code += sourceCode[i] + '\n';
|
|
71
|
+
}
|
|
72
|
+
return code;
|
|
73
|
+
}, [startLine, endLine, sourceCode]);
|
|
43
74
|
|
|
44
75
|
useEffect(() => {
|
|
45
76
|
setActionButtons?.(
|
|
@@ -64,6 +95,13 @@ export const SourceView = React.memo(function SourceView({
|
|
|
64
95
|
return <>Source code not uploaded for this build.</>;
|
|
65
96
|
}
|
|
66
97
|
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
const onContextMenu = (event: any): void => {
|
|
100
|
+
show({
|
|
101
|
+
event,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
67
105
|
const table = tableFromIPC(data.record);
|
|
68
106
|
const cumulative = table.getChild('cumulative');
|
|
69
107
|
const flat = table.getChild('flat');
|
|
@@ -80,8 +118,17 @@ export const SourceView = React.memo(function SourceView({
|
|
|
80
118
|
<Highlighter
|
|
81
119
|
file={sourceFileName as string}
|
|
82
120
|
content={data.source}
|
|
83
|
-
renderer={profileAwareRenderer(cumulative, flat, total, filtered)}
|
|
121
|
+
renderer={profileAwareRenderer(cumulative, flat, total, filtered, onContextMenu)}
|
|
84
122
|
/>
|
|
123
|
+
{sourceViewContextMenuItems.length > 0 ? (
|
|
124
|
+
<Menu id={MENU_ID}>
|
|
125
|
+
{sourceViewContextMenuItems.map(item => (
|
|
126
|
+
<Item key={item.id} onClick={() => item.action(selectedCode)}>
|
|
127
|
+
{item.label}
|
|
128
|
+
</Item>
|
|
129
|
+
))}
|
|
130
|
+
</Menu>
|
|
131
|
+
) : null}
|
|
85
132
|
</motion.div>
|
|
86
133
|
</AnimatePresence>
|
|
87
134
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
|
|
14
|
+
import {useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
import {useURLState} from '@parca/components';
|
|
17
|
+
|
|
18
|
+
interface LineRange {
|
|
19
|
+
startLine: number;
|
|
20
|
+
endLine: number;
|
|
21
|
+
setLineRange: (start: number, end: number) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const useLineRange = (): LineRange => {
|
|
25
|
+
const [sourceLine, setSourceLine] = useURLState({param: 'source_line', navigateTo: () => {}});
|
|
26
|
+
const [startLine, endLine] = useMemo(() => {
|
|
27
|
+
if (sourceLine == null) {
|
|
28
|
+
return [-1, -1];
|
|
29
|
+
}
|
|
30
|
+
const [start, end] = (sourceLine as string).split('-');
|
|
31
|
+
return [parseInt(start, 10), parseInt(end, 10)];
|
|
32
|
+
}, [sourceLine]);
|
|
33
|
+
|
|
34
|
+
const setLineRange = (start: number, end: number): void => {
|
|
35
|
+
setSourceLine(`${start}-${end}`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {startLine, endLine, setLineRange};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default useLineRange;
|