@opensumi/ide-comments 2.21.13 → 2.22.0
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/lib/browser/comments-feature.registry.js.map +1 -1
- package/lib/browser/comments-panel.view.d.ts +2 -3
- package/lib/browser/comments-panel.view.d.ts.map +1 -1
- package/lib/browser/comments-panel.view.js +32 -70
- package/lib/browser/comments-panel.view.js.map +1 -1
- package/lib/browser/comments-thread.d.ts +4 -2
- package/lib/browser/comments-thread.d.ts.map +1 -1
- package/lib/browser/comments-thread.js +14 -7
- package/lib/browser/comments-thread.js.map +1 -1
- package/lib/browser/comments-zone.service.js.map +1 -1
- package/lib/browser/comments-zone.view.d.ts +1 -1
- package/lib/browser/comments-zone.view.d.ts.map +1 -1
- package/lib/browser/comments-zone.view.js +1 -1
- package/lib/browser/comments-zone.view.js.map +1 -1
- package/lib/browser/comments.contribution.d.ts +1 -0
- package/lib/browser/comments.contribution.d.ts.map +1 -1
- package/lib/browser/comments.contribution.js +6 -1
- package/lib/browser/comments.contribution.js.map +1 -1
- package/lib/browser/comments.service.d.ts +7 -4
- package/lib/browser/comments.service.d.ts.map +1 -1
- package/lib/browser/comments.service.js +60 -87
- package/lib/browser/comments.service.js.map +1 -1
- package/lib/browser/index.d.ts +0 -1
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +5 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/tree/comment-node.d.ts +14 -0
- package/lib/browser/tree/comment-node.d.ts.map +1 -0
- package/lib/browser/tree/comment-node.js +64 -0
- package/lib/browser/tree/comment-node.js.map +1 -0
- package/lib/browser/tree/tree-model.service.d.ts +43 -0
- package/lib/browser/tree/tree-model.service.d.ts.map +1 -0
- package/lib/browser/tree/tree-model.service.js +150 -0
- package/lib/browser/tree/tree-model.service.js.map +1 -0
- package/lib/browser/tree/tree-node.defined.d.ts +61 -0
- package/lib/browser/tree/tree-node.defined.d.ts.map +1 -0
- package/lib/browser/tree/tree-node.defined.js +116 -0
- package/lib/browser/tree/tree-node.defined.js.map +1 -0
- package/lib/browser/tree/tree-node.module.less +154 -0
- package/lib/common/index.d.ts +38 -36
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js.map +1 -1
- package/package.json +13 -12
- package/src/browser/comment-reactions.view.tsx +109 -0
- package/src/browser/comments-body.tsx +57 -0
- package/src/browser/comments-feature.registry.ts +91 -0
- package/src/browser/comments-item.view.tsx +362 -0
- package/src/browser/comments-panel.view.tsx +90 -0
- package/src/browser/comments-textarea.view.tsx +194 -0
- package/src/browser/comments-thread.ts +309 -0
- package/src/browser/comments-zone.service.ts +29 -0
- package/src/browser/comments-zone.view.tsx +206 -0
- package/src/browser/comments.contribution.ts +201 -0
- package/src/browser/comments.module.less +210 -0
- package/src/browser/comments.service.ts +546 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/markdown.style.ts +25 -0
- package/src/browser/mentions.style.ts +55 -0
- package/src/browser/tree/comment-node.tsx +130 -0
- package/src/browser/tree/tree-model.service.ts +173 -0
- package/src/browser/tree/tree-node.defined.ts +167 -0
- package/src/browser/tree/tree-node.module.less +154 -0
- package/src/common/index.ts +710 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import clx from 'classnames';
|
|
2
|
+
import React, { FC, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { IRecycleTreeHandle, RecycleTree } from '@opensumi/ide-components';
|
|
5
|
+
import { useInjectable, ViewState, localize } from '@opensumi/ide-core-browser';
|
|
6
|
+
|
|
7
|
+
import { ICommentsFeatureRegistry } from '../common';
|
|
8
|
+
|
|
9
|
+
import styles from './comments.module.less';
|
|
10
|
+
import { CommentNodeRendered, COMMENT_TREE_NODE_HEIGHT, ICommentNodeRenderedProps } from './tree/comment-node';
|
|
11
|
+
import { CommentModelService, CommentTreeModel } from './tree/tree-model.service';
|
|
12
|
+
|
|
13
|
+
export const CommentsPanel: FC<{ viewState: ViewState }> = ({ viewState }) => {
|
|
14
|
+
const commentModelService = useInjectable<CommentModelService>(CommentModelService);
|
|
15
|
+
const [model, setModel] = useState<CommentTreeModel | undefined>();
|
|
16
|
+
const wrapperRef: RefObject<HTMLDivElement> = useRef(null);
|
|
17
|
+
|
|
18
|
+
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
19
|
+
|
|
20
|
+
const { handleTreeBlur } = commentModelService;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setModel(commentModelService.treeModel);
|
|
24
|
+
const disposable = commentModelService.onDidUpdateTreeModel((model?: CommentTreeModel) => {
|
|
25
|
+
setModel(model);
|
|
26
|
+
});
|
|
27
|
+
return () => {
|
|
28
|
+
disposable.dispose();
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const handleTreeReady = useCallback(
|
|
33
|
+
(handle: IRecycleTreeHandle) => {
|
|
34
|
+
commentModelService.handleTreeHandler(handle);
|
|
35
|
+
},
|
|
36
|
+
[commentModelService],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const renderTreeNode = useCallback(
|
|
40
|
+
(props: ICommentNodeRenderedProps) => (
|
|
41
|
+
<CommentNodeRendered
|
|
42
|
+
item={props.item}
|
|
43
|
+
itemType={props.itemType}
|
|
44
|
+
decorations={commentModelService.decorations.getDecorations(props.item as any)}
|
|
45
|
+
defaultLeftPadding={8}
|
|
46
|
+
onClick={commentModelService.handleItemClick}
|
|
47
|
+
leftPadding={8}
|
|
48
|
+
/>
|
|
49
|
+
),
|
|
50
|
+
[model],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const commentsPanelOptions = useMemo(() => commentsFeatureRegistry.getCommentsPanelOptions(), []);
|
|
54
|
+
|
|
55
|
+
const headerComponent = useMemo(() => commentsPanelOptions.header, [commentsPanelOptions]);
|
|
56
|
+
|
|
57
|
+
const defaultPlaceholder = useMemo(
|
|
58
|
+
() => (
|
|
59
|
+
<div className={styles.panel_placeholder}>
|
|
60
|
+
{commentsPanelOptions.defaultPlaceholder || localize('comments.panel.placeholder')}
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
[commentsPanelOptions],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const renderSearchTree = useCallback(() => {
|
|
67
|
+
if (model) {
|
|
68
|
+
return (
|
|
69
|
+
<RecycleTree
|
|
70
|
+
height={viewState.height - (headerComponent?.height || 0)}
|
|
71
|
+
itemHeight={COMMENT_TREE_NODE_HEIGHT}
|
|
72
|
+
onReady={handleTreeReady}
|
|
73
|
+
model={model}
|
|
74
|
+
placeholder={() => defaultPlaceholder}
|
|
75
|
+
>
|
|
76
|
+
{renderTreeNode}
|
|
77
|
+
</RecycleTree>
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
return defaultPlaceholder;
|
|
81
|
+
}
|
|
82
|
+
}, [model, headerComponent, viewState.height]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={styles.comment_panel} tabIndex={-1} onBlur={handleTreeBlur} ref={wrapperRef}>
|
|
86
|
+
{headerComponent?.component}
|
|
87
|
+
{renderSearchTree()}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MentionsInput, Mention } from 'react-mentions';
|
|
3
|
+
|
|
4
|
+
import { Tabs } from '@opensumi/ide-components';
|
|
5
|
+
import { localize, useInjectable } from '@opensumi/ide-core-browser';
|
|
6
|
+
|
|
7
|
+
import { ICommentsFeatureRegistry } from '../common';
|
|
8
|
+
|
|
9
|
+
import { CommentsBody } from './comments-body';
|
|
10
|
+
import styles from './comments.module.less';
|
|
11
|
+
import { getMentionBoxStyle } from './mentions.style';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export interface ICommentTextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
15
|
+
focusDelay?: number;
|
|
16
|
+
minRows?: number;
|
|
17
|
+
maxRows?: number;
|
|
18
|
+
initialHeight?: string;
|
|
19
|
+
value: string;
|
|
20
|
+
dragFiles?: (files: FileList) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultTrigger = '@';
|
|
24
|
+
const defaultMarkup = '@[__display__](__id__)';
|
|
25
|
+
const defaultDisplayTransform = (id: string, display: string) => `@${display}`;
|
|
26
|
+
|
|
27
|
+
export const CommentsTextArea = React.forwardRef<HTMLTextAreaElement, ICommentTextAreaProps>((props, ref) => {
|
|
28
|
+
const {
|
|
29
|
+
focusDelay = 0,
|
|
30
|
+
autoFocus = false,
|
|
31
|
+
placeholder = '',
|
|
32
|
+
onFocus,
|
|
33
|
+
onBlur,
|
|
34
|
+
onChange,
|
|
35
|
+
maxRows = 10,
|
|
36
|
+
minRows = 2,
|
|
37
|
+
value,
|
|
38
|
+
initialHeight,
|
|
39
|
+
dragFiles,
|
|
40
|
+
} = props;
|
|
41
|
+
const [index, setIndex] = React.useState(0);
|
|
42
|
+
const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
|
|
43
|
+
const inputRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
44
|
+
const mentionsRef = React.useRef<HTMLDivElement | null>(null);
|
|
45
|
+
const itemRef = React.useRef<HTMLDivElement | null>(null);
|
|
46
|
+
// make `ref` to input works
|
|
47
|
+
React.useImperativeHandle(ref, () => inputRef.current!);
|
|
48
|
+
|
|
49
|
+
const handleFileSelect = React.useCallback(
|
|
50
|
+
async (event: DragEvent) => {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
|
|
54
|
+
const files = event.dataTransfer?.files; // FileList object.
|
|
55
|
+
if (files && dragFiles) {
|
|
56
|
+
await dragFiles(files);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (inputRef.current) {
|
|
60
|
+
inputRef.current.focus();
|
|
61
|
+
selectLastPosition(inputRef.current.value);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[dragFiles],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleDragOver = React.useCallback((event) => {
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const selectLastPosition = React.useCallback((value) => {
|
|
74
|
+
const textarea = inputRef.current;
|
|
75
|
+
if (textarea) {
|
|
76
|
+
const position = value.toString().length;
|
|
77
|
+
textarea.setSelectionRange(position, position);
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
const textarea = inputRef.current;
|
|
83
|
+
if (!textarea) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (initialHeight && textarea.style) {
|
|
87
|
+
textarea.style.height = initialHeight;
|
|
88
|
+
}
|
|
89
|
+
if (focusDelay) {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
textarea.focus({
|
|
92
|
+
preventScroll: true,
|
|
93
|
+
});
|
|
94
|
+
}, focusDelay);
|
|
95
|
+
}
|
|
96
|
+
// auto set last selection
|
|
97
|
+
selectLastPosition(value);
|
|
98
|
+
function handleMouseWheel(event: Event) {
|
|
99
|
+
const target = event.target as Element;
|
|
100
|
+
if (target) {
|
|
101
|
+
if (
|
|
102
|
+
// 当前文本框出现滚动时,防止被编辑器滚动拦截,阻止冒泡
|
|
103
|
+
(target.nodeName.toUpperCase() === 'TEXTAREA' && target.scrollHeight > target.clientHeight) ||
|
|
104
|
+
// 当是在弹出的提及里滚动,防止被编辑器滚动拦截,阻止冒泡
|
|
105
|
+
target.nodeName.toUpperCase() === 'UL' ||
|
|
106
|
+
target.parentElement?.nodeName.toUpperCase() === 'UL' ||
|
|
107
|
+
target.parentElement?.parentElement?.nodeName.toUpperCase() === 'UL'
|
|
108
|
+
) {
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
mentionsRef.current?.addEventListener('mousewheel', handleMouseWheel, true);
|
|
114
|
+
return () => {
|
|
115
|
+
mentionsRef.current?.removeEventListener('mousewheel', handleMouseWheel, true);
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
React.useEffect(() => {
|
|
120
|
+
if (index === 0) {
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
inputRef.current?.focus({
|
|
123
|
+
preventScroll: true,
|
|
124
|
+
});
|
|
125
|
+
}, focusDelay);
|
|
126
|
+
selectLastPosition(value);
|
|
127
|
+
}
|
|
128
|
+
}, [index]);
|
|
129
|
+
|
|
130
|
+
const style = React.useMemo(
|
|
131
|
+
() =>
|
|
132
|
+
getMentionBoxStyle({
|
|
133
|
+
minRows,
|
|
134
|
+
maxRows,
|
|
135
|
+
}),
|
|
136
|
+
[minRows, maxRows],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const mentionsOptions = React.useMemo(() => commentsFeatureRegistry.getMentionsOptions(), [commentsFeatureRegistry]);
|
|
140
|
+
|
|
141
|
+
const providerData = React.useCallback(
|
|
142
|
+
async (query: string, callback) => {
|
|
143
|
+
if (mentionsOptions.providerData) {
|
|
144
|
+
const data = await mentionsOptions.providerData(query);
|
|
145
|
+
callback(data);
|
|
146
|
+
} else {
|
|
147
|
+
callback([]);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[mentionsOptions],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className={styles.textarea_container}>
|
|
155
|
+
<Tabs
|
|
156
|
+
mini
|
|
157
|
+
value={index}
|
|
158
|
+
onChange={(index: number) => setIndex(index)}
|
|
159
|
+
tabs={[localize('comments.thread.textarea.write'), localize('comments.thread.textarea.preview')]}
|
|
160
|
+
/>
|
|
161
|
+
<div>
|
|
162
|
+
{index === 0 ? (
|
|
163
|
+
<div ref={mentionsRef}>
|
|
164
|
+
<MentionsInput
|
|
165
|
+
autoFocus={autoFocus}
|
|
166
|
+
onDragOver={handleDragOver}
|
|
167
|
+
onDrop={handleFileSelect}
|
|
168
|
+
inputRef={inputRef}
|
|
169
|
+
ref={itemRef}
|
|
170
|
+
value={value}
|
|
171
|
+
placeholder={placeholder}
|
|
172
|
+
onChange={onChange}
|
|
173
|
+
onFocus={onFocus}
|
|
174
|
+
onBlur={onBlur}
|
|
175
|
+
style={style}
|
|
176
|
+
>
|
|
177
|
+
<Mention
|
|
178
|
+
markup={mentionsOptions.markup || defaultMarkup}
|
|
179
|
+
renderSuggestion={mentionsOptions.renderSuggestion}
|
|
180
|
+
trigger={defaultTrigger}
|
|
181
|
+
data={providerData}
|
|
182
|
+
displayTransform={mentionsOptions.displayTransform || defaultDisplayTransform}
|
|
183
|
+
/>
|
|
184
|
+
</MentionsInput>
|
|
185
|
+
</div>
|
|
186
|
+
) : (
|
|
187
|
+
<div className={styles.textarea_preview}>
|
|
188
|
+
<CommentsBody body={value} />
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { observable, computed, autorun } from 'mobx';
|
|
2
|
+
|
|
3
|
+
import { Injectable, Autowired, INJECTOR_TOKEN, Injector } from '@opensumi/di';
|
|
4
|
+
import { IRange, Disposable, URI, IContextKeyService, uuid, localize, Emitter } from '@opensumi/ide-core-browser';
|
|
5
|
+
import { ResourceContextKey } from '@opensumi/ide-core-browser/lib/contextkey/resource';
|
|
6
|
+
import { IEditor, EditorCollectionService } from '@opensumi/ide-editor';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ICommentsThread,
|
|
10
|
+
IComment,
|
|
11
|
+
ICommentsThreadOptions,
|
|
12
|
+
ICommentsService,
|
|
13
|
+
IThreadComment,
|
|
14
|
+
ICommentsZoneWidget,
|
|
15
|
+
} from '../common';
|
|
16
|
+
|
|
17
|
+
import { CommentsZoneWidget } from './comments-zone.view';
|
|
18
|
+
|
|
19
|
+
@Injectable({ multiple: true })
|
|
20
|
+
export class CommentsThread extends Disposable implements ICommentsThread {
|
|
21
|
+
@Autowired(ICommentsService)
|
|
22
|
+
commentsService: ICommentsService;
|
|
23
|
+
|
|
24
|
+
@Autowired(IContextKeyService)
|
|
25
|
+
private readonly globalContextKeyService: IContextKeyService;
|
|
26
|
+
|
|
27
|
+
private readonly _contextKeyService: IContextKeyService;
|
|
28
|
+
|
|
29
|
+
@Autowired(EditorCollectionService)
|
|
30
|
+
private readonly editorCollectionService: EditorCollectionService;
|
|
31
|
+
|
|
32
|
+
@Autowired(INJECTOR_TOKEN)
|
|
33
|
+
private readonly injector: Injector;
|
|
34
|
+
|
|
35
|
+
// FIXME: update by https://github.com/opensumi/core/blob/82ab63b916c8fe90cf5898d55c0fe335dd852b91/packages/extension/src/browser/vscode/api/main.thread.comments.ts#L319
|
|
36
|
+
@observable
|
|
37
|
+
public comments: IThreadComment[];
|
|
38
|
+
|
|
39
|
+
@observable
|
|
40
|
+
public label: string | undefined;
|
|
41
|
+
|
|
42
|
+
@observable
|
|
43
|
+
private _readOnly = false;
|
|
44
|
+
|
|
45
|
+
@observable
|
|
46
|
+
public isCollapsed: boolean;
|
|
47
|
+
|
|
48
|
+
public data: any;
|
|
49
|
+
|
|
50
|
+
set contextValue(value: string | undefined) {
|
|
51
|
+
this._contextKeyService.createKey<string>('thread', value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get contextValue() {
|
|
55
|
+
return this._contextKeyService.getContextValue('thread');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private widgets = new Map<IEditor, CommentsZoneWidget>();
|
|
59
|
+
|
|
60
|
+
private _id = `thread_${uuid()}`;
|
|
61
|
+
|
|
62
|
+
private onDidChangeEmitter: Emitter<void> = new Emitter();
|
|
63
|
+
|
|
64
|
+
get onDidChange() {
|
|
65
|
+
return this.onDidChangeEmitter.event;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
public uri: URI,
|
|
70
|
+
public range: IRange,
|
|
71
|
+
public providerId: string,
|
|
72
|
+
public options: ICommentsThreadOptions,
|
|
73
|
+
) {
|
|
74
|
+
super();
|
|
75
|
+
this.comments = options.comments
|
|
76
|
+
? options.comments.map((comment) => ({
|
|
77
|
+
...comment,
|
|
78
|
+
id: uuid(),
|
|
79
|
+
}))
|
|
80
|
+
: [];
|
|
81
|
+
this.data = this.options.data;
|
|
82
|
+
this._contextKeyService = this.registerDispose(this.globalContextKeyService.createScoped());
|
|
83
|
+
// 设置 resource context key
|
|
84
|
+
const resourceContext = new ResourceContextKey(this._contextKeyService);
|
|
85
|
+
resourceContext.set(uri);
|
|
86
|
+
this._contextKeyService.createKey<string>('thread', options.contextValue);
|
|
87
|
+
this.readOnly = !!options.readOnly;
|
|
88
|
+
this.label = options.label;
|
|
89
|
+
this.isCollapsed = !!this.options.isCollapsed;
|
|
90
|
+
const threadsLengthContext = this._contextKeyService.createKey<number>(
|
|
91
|
+
'threadsLength',
|
|
92
|
+
this.commentsService.getThreadsByUri(uri).length,
|
|
93
|
+
);
|
|
94
|
+
const commentsLengthContext = this._contextKeyService.createKey<number>('commentsLength', this.comments.length);
|
|
95
|
+
// vscode 用于判断 thread 是否为空
|
|
96
|
+
const commentThreadIsEmptyContext = this._contextKeyService.createKey<boolean>(
|
|
97
|
+
'commentThreadIsEmpty',
|
|
98
|
+
!this.comments.length,
|
|
99
|
+
);
|
|
100
|
+
// vscode 用于判断是否为当前 controller 注册
|
|
101
|
+
this._contextKeyService.createKey<string>('commentController', providerId);
|
|
102
|
+
// 监听 comments 的变化
|
|
103
|
+
autorun(() => {
|
|
104
|
+
commentsLengthContext.set(this.comments.length);
|
|
105
|
+
commentThreadIsEmptyContext.set(!this.comments.length);
|
|
106
|
+
});
|
|
107
|
+
autorun(() => {
|
|
108
|
+
if (this.isCollapsed) {
|
|
109
|
+
this.hideAll();
|
|
110
|
+
} else {
|
|
111
|
+
this.showAll();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// 监听每次 thread 的变化,重新设置 threadsLength
|
|
115
|
+
this.addDispose(
|
|
116
|
+
this.commentsService.onThreadsChanged((thread) => {
|
|
117
|
+
if (thread.uri.isEqual(uri)) {
|
|
118
|
+
threadsLengthContext.set(this.commentsService.getThreadsByUri(uri).length);
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
this.addDispose({
|
|
123
|
+
dispose: () => {
|
|
124
|
+
this.comments = [];
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
this.onDidChangeEmitter.fire();
|
|
128
|
+
}
|
|
129
|
+
getWidgetByEditor(editor: IEditor): ICommentsZoneWidget | undefined {
|
|
130
|
+
return this.widgets.get(editor);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get id() {
|
|
134
|
+
return this._id;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get contextKeyService() {
|
|
138
|
+
return this._contextKeyService;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@computed
|
|
142
|
+
get readOnly() {
|
|
143
|
+
return this._readOnly;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set readOnly(readOnly: boolean) {
|
|
147
|
+
this._readOnly = readOnly;
|
|
148
|
+
this._contextKeyService.createKey<boolean>('readOnly', this._readOnly);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@computed
|
|
152
|
+
get threadHeaderTitle() {
|
|
153
|
+
if (this.label) {
|
|
154
|
+
return this.label;
|
|
155
|
+
}
|
|
156
|
+
if (this.comments.length) {
|
|
157
|
+
const commentAuthors = new Set<string>(this.comments.map((comment) => `@${comment.author.name}`));
|
|
158
|
+
return `${localize('comments.participants')}: ` + [...commentAuthors].join(' ');
|
|
159
|
+
} else {
|
|
160
|
+
return localize('comments.zone.title');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getEditorsByUri(uri: URI): IEditor[] {
|
|
165
|
+
return this.editorCollectionService.listEditors().filter((editor) => editor.currentUri?.isEqual(uri));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private addWidgetByEditor(editor: IEditor) {
|
|
169
|
+
const widget = this.injector.get(CommentsZoneWidget, [editor, this]);
|
|
170
|
+
// 如果当前 widget 发生高度变化,通知同一个 同一个 editor 的其他 range 相同的 thread 也重新计算一下高度
|
|
171
|
+
this.addDispose(
|
|
172
|
+
widget.onChangeZoneWidget(() => {
|
|
173
|
+
const threads = this.commentsService.commentsThreads.filter((thread) => this.isEqual(thread));
|
|
174
|
+
// 只需要 resize 当前 thread 之后的 thread
|
|
175
|
+
const currentIndex = threads.findIndex((thread) => thread === this);
|
|
176
|
+
const resizeThreads = threads.slice(currentIndex + 1);
|
|
177
|
+
for (const thread of resizeThreads) {
|
|
178
|
+
if (thread.isShowWidget(editor)) {
|
|
179
|
+
const widget = thread.getWidgetByEditor(editor);
|
|
180
|
+
widget?.resize();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
this.addDispose(widget);
|
|
186
|
+
this.widgets.set(editor, widget);
|
|
187
|
+
editor.onDispose(() => {
|
|
188
|
+
widget.dispose();
|
|
189
|
+
this.widgets.delete(editor);
|
|
190
|
+
});
|
|
191
|
+
return widget;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public toggle = (editor: IEditor) => {
|
|
195
|
+
if (this.comments.length > 0) {
|
|
196
|
+
const widget = this.widgets.get(editor);
|
|
197
|
+
if (widget) {
|
|
198
|
+
widget.toggle();
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
this.dispose();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
public show(editor?: IEditor) {
|
|
206
|
+
if (editor) {
|
|
207
|
+
let widget = this.widgets.get(editor);
|
|
208
|
+
// 说明是在新的 group 中打开
|
|
209
|
+
if (!widget) {
|
|
210
|
+
widget = this.addWidgetByEditor(editor);
|
|
211
|
+
}
|
|
212
|
+
widget.show();
|
|
213
|
+
} else {
|
|
214
|
+
// 每次都拿所有的有这个 uri 的 editor
|
|
215
|
+
const editors = this.getEditorsByUri(this.uri);
|
|
216
|
+
editors.forEach((editor) => {
|
|
217
|
+
let widget = this.widgets.get(editor);
|
|
218
|
+
// 说明是在新的 group 中打开
|
|
219
|
+
if (!widget) {
|
|
220
|
+
widget = this.addWidgetByEditor(editor);
|
|
221
|
+
}
|
|
222
|
+
// 如果标记之前是已经展示的 widget,则调用 show 方法
|
|
223
|
+
if (editor.currentUri?.isEqual(this.uri)) {
|
|
224
|
+
widget.show();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public showWidgetsIfShowed() {
|
|
231
|
+
for (const editor of this.getEditorsByUri(this.uri)) {
|
|
232
|
+
let widget = this.widgets.get(editor);
|
|
233
|
+
// 说明是在新的 group 中打开
|
|
234
|
+
if (!widget) {
|
|
235
|
+
widget = this.addWidgetByEditor(editor);
|
|
236
|
+
}
|
|
237
|
+
// 如果标记之前是已经展示的 widget,则调用 show 方法
|
|
238
|
+
if (editor.currentUri?.isEqual(this.uri) && widget.isShow) {
|
|
239
|
+
widget.show();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public hideWidgetsByDispose(): void {
|
|
245
|
+
for (const [editor, widget] of this.widgets) {
|
|
246
|
+
!editor.currentUri?.isEqual(this.uri) && widget.dispose();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public isShowWidget(editor?: IEditor) {
|
|
251
|
+
if (editor) {
|
|
252
|
+
const widget = this.widgets.get(editor);
|
|
253
|
+
return widget ? widget.isShow : false;
|
|
254
|
+
} else {
|
|
255
|
+
for (const [, widget] of this.widgets) {
|
|
256
|
+
return widget.isShow;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public hide(editor?: IEditor) {
|
|
263
|
+
if (editor) {
|
|
264
|
+
const widget = this.widgets.get(editor);
|
|
265
|
+
widget?.hide();
|
|
266
|
+
} else {
|
|
267
|
+
this.hideAll();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public showAll() {
|
|
272
|
+
for (const [, widget] of this.widgets) {
|
|
273
|
+
widget.show();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public hideAll(isDospose?: boolean) {
|
|
278
|
+
for (const [editor, widget] of this.widgets) {
|
|
279
|
+
if (isDospose) {
|
|
280
|
+
// 如果 thread 出现在当前 editor 则不隐藏
|
|
281
|
+
!editor.currentUri?.isEqual(this.uri) && widget.dispose();
|
|
282
|
+
} else {
|
|
283
|
+
widget.hide();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public addComment(...comments: IComment[]) {
|
|
289
|
+
this.comments.push(
|
|
290
|
+
...comments.map((comment) => ({
|
|
291
|
+
...comment,
|
|
292
|
+
id: uuid(),
|
|
293
|
+
})),
|
|
294
|
+
);
|
|
295
|
+
this.onDidChangeEmitter.fire();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public removeComment(comment: IComment) {
|
|
299
|
+
const index = this.comments.findIndex((c) => c === comment);
|
|
300
|
+
if (index !== -1) {
|
|
301
|
+
this.comments.splice(index, 1);
|
|
302
|
+
}
|
|
303
|
+
this.onDidChangeEmitter.fire();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public isEqual(thread: ICommentsThread): boolean {
|
|
307
|
+
return thread.uri.isEqual(this.uri) && thread.range.startLineNumber === this.range.startLineNumber;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Autowired, Injectable, Optional } from '@opensumi/di';
|
|
2
|
+
import { AbstractMenuService, MenuId, IMenu } from '@opensumi/ide-core-browser/lib/menu/next';
|
|
3
|
+
import { Disposable, memoize } from '@opensumi/ide-core-common';
|
|
4
|
+
|
|
5
|
+
import { CommentsThread } from './comments-thread';
|
|
6
|
+
|
|
7
|
+
@Injectable({ multiple: true })
|
|
8
|
+
export class CommentsZoneService extends Disposable {
|
|
9
|
+
@Autowired(AbstractMenuService)
|
|
10
|
+
private readonly menuService: AbstractMenuService;
|
|
11
|
+
|
|
12
|
+
constructor(@Optional() readonly thread: CommentsThread) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@memoize
|
|
17
|
+
get commentThreadTitle(): IMenu {
|
|
18
|
+
return this.registerDispose(
|
|
19
|
+
this.menuService.createMenu(MenuId.CommentsCommentThreadTitle, this.thread.contextKeyService),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@memoize
|
|
24
|
+
get commentThreadContext(): IMenu {
|
|
25
|
+
return this.registerDispose(
|
|
26
|
+
this.menuService.createMenu(MenuId.CommentsCommentThreadContext, this.thread.contextKeyService),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|