feat(editor): can highlight resolved comment (#13122)

#### PR Dependency Tree


* **PR #13122** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Inline comments now visually distinguish between unresolved, resolved,
and deleted states.
* Only unresolved inline comments are interactive and highlighted in the
editor.

* **Bug Fixes**
* Improved accuracy in fetching and displaying all comments, including
resolved ones, during initialization.

* **Refactor**
* Enhanced handling of comment resolution and deletion to provide
clearer differentiation in both behavior and appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-10 11:06:05 +08:00
committed by GitHub
parent 0f3066f7d0
commit 1fe07410c0
4 changed files with 60 additions and 21 deletions

View File

@@ -43,10 +43,14 @@ export class InlineCommentManager extends LifeCycleWatcher {
this._disposables.add(provider.onCommentAdded(this._handleAddComment)); this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add( this._disposables.add(
provider.onCommentDeleted(this._handleDeleteAndResolve) provider.onCommentDeleted(id =>
this._handleDeleteAndResolve(id, 'delete')
)
); );
this._disposables.add( this._disposables.add(
provider.onCommentResolved(this._handleDeleteAndResolve) provider.onCommentResolved(id =>
this._handleDeleteAndResolve(id, 'resolve')
)
); );
this._disposables.add( this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment) provider.onCommentHighlighted(this._handleHighlightComment)
@@ -64,15 +68,16 @@ export class InlineCommentManager extends LifeCycleWatcher {
const provider = this._provider; const provider = this._provider;
if (!provider) return; if (!provider) return;
const commentsInProvider = await provider.getComments('unresolved'); const commentsInProvider = await provider.getComments('all');
const commentsInEditor = this.getCommentsInEditor(); const commentsInEditor = this.getCommentsInEditor();
// remove comments that are in editor but not in provider // remove comments that are in editor but not in provider
// which means the comment may be removed or resolved in provider side // which means the comment may be removed or resolved in provider side
difference(commentsInEditor, commentsInProvider).forEach(comment => { difference(commentsInEditor, commentsInProvider).forEach(comment => {
this._handleDeleteAndResolve(comment); this.std
this.std.get(BlockElementCommentManager).handleDeleteAndResolve(comment); .get(BlockElementCommentManager)
.handleDeleteAndResolve(comment, 'delete');
}); });
} }
@@ -162,7 +167,10 @@ export class InlineCommentManager extends LifeCycleWatcher {
}); });
}; };
private readonly _handleDeleteAndResolve = (id: CommentId) => { private readonly _handleDeleteAndResolve = (
id: CommentId,
type: 'delete' | 'resolve'
) => {
const commentedTexts = findCommentedTexts(this.std.store, id); const commentedTexts = findCommentedTexts(this.std.store, id);
if (commentedTexts.length === 0) return; if (commentedTexts.length === 0) return;
@@ -176,7 +184,7 @@ export class InlineCommentManager extends LifeCycleWatcher {
inlineEditor?.formatText( inlineEditor?.formatText(
selection.from, selection.from,
{ {
[`comment-${id}`]: null, [`comment-${id}`]: type === 'delete' ? null : false,
}, },
{ {
withoutTransact: true, withoutTransact: true,

View File

@@ -22,7 +22,7 @@ import { isEqual } from 'lodash-es';
}) })
export class InlineComment extends WithDisposable(ShadowlessElement) { export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css` static override styles = css`
inline-comment { inline-comment.unresolved {
display: inline-block; display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')}; background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid border-bottom: 2px solid
@@ -41,6 +41,9 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
}) })
accessor commentIds!: string[]; accessor commentIds!: string[];
@property({ attribute: false })
accessor unresolved = false;
private _index: number = 0; private _index: number = 0;
@consume({ context: stdContext }) @consume({ context: stdContext })
@@ -54,8 +57,10 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
} }
private readonly _handleClick = () => { private readonly _handleClick = () => {
this._provider?.highlightComment(this.commentIds[this._index]); if (this.unresolved) {
this._index = (this._index + 1) % this.commentIds.length; this._provider?.highlightComment(this.commentIds[this._index]);
this._index = (this._index + 1) % this.commentIds.length;
}
}; };
private readonly _handleHighlight = (id: CommentId | null) => { private readonly _handleHighlight = (id: CommentId | null) => {
@@ -89,6 +94,13 @@ export class InlineComment extends WithDisposable(ShadowlessElement) {
this.classList.remove('highlighted'); this.classList.remove('highlighted');
} }
} }
if (_changedProperties.has('unresolved')) {
if (this.unresolved) {
this.classList.add('unresolved');
} else {
this.classList.remove('unresolved');
}
}
} }
override render() { override render() {

View File

@@ -21,19 +21,25 @@ export const CommentInlineSpecExtension =
), ),
match: delta => { match: delta => {
if (!delta.attributes) return false; if (!delta.attributes) return false;
const comments = Object.entries(delta.attributes).filter( const comments = Object.keys(delta.attributes).filter(isInlineCommendId);
([key, value]) => isInlineCommendId(key) && value === true
);
return comments.length > 0; return comments.length > 0;
}, },
renderer: ({ delta, children }) => renderer: ({ delta, children }) => {
html`<inline-comment .commentIds=${extractCommentIdFromDelta(delta)} if (!delta.attributes) return html`${nothing}`;
const unresolved = Object.entries(delta.attributes).some(
([key, value]) => isInlineCommendId(key) && value === true
);
return html`<inline-comment
.unresolved=${unresolved}
.commentIds=${extractCommentIdFromDelta(delta)}
>${when( >${when(
children, children,
() => html`${children}`, () => html`${children}`,
() => nothing () => nothing
)}</inline-comment )}</inline-comment
>`, >`;
},
wrapper: true, wrapper: true,
}); });

View File

@@ -57,10 +57,12 @@ export class BlockElementCommentManager extends LifeCycleWatcher {
this._disposables.add(provider.onCommentAdded(this._handleAddComment)); this._disposables.add(provider.onCommentAdded(this._handleAddComment));
this._disposables.add( this._disposables.add(
provider.onCommentDeleted(this.handleDeleteAndResolve) provider.onCommentDeleted(id => this.handleDeleteAndResolve(id, 'delete'))
); );
this._disposables.add( this._disposables.add(
provider.onCommentResolved(this.handleDeleteAndResolve) provider.onCommentResolved(id =>
this.handleDeleteAndResolve(id, 'resolve')
)
); );
this._disposables.add( this._disposables.add(
provider.onCommentHighlighted(this._handleHighlightComment) provider.onCommentHighlighted(this._handleHighlightComment)
@@ -146,18 +148,29 @@ export class BlockElementCommentManager extends LifeCycleWatcher {
} }
}; };
readonly handleDeleteAndResolve = (id: CommentId) => { readonly handleDeleteAndResolve = (
id: CommentId,
type: 'delete' | 'resolve'
) => {
const commentedBlocks = findCommentedBlocks(this.std.store, id); const commentedBlocks = findCommentedBlocks(this.std.store, id);
this.std.store.withoutTransact(() => { this.std.store.withoutTransact(() => {
commentedBlocks.forEach(block => { commentedBlocks.forEach(block => {
delete block.props.comments[id]; if (type === 'delete') {
delete block.props.comments[id];
} else {
block.props.comments[id] = false;
}
}); });
}); });
const commentedElements = findCommentedElements(this.std.store, id); const commentedElements = findCommentedElements(this.std.store, id);
this.std.store.withoutTransact(() => { this.std.store.withoutTransact(() => {
commentedElements.forEach(element => { commentedElements.forEach(element => {
delete element.comments[id]; if (type === 'delete') {
delete element.comments[id];
} else {
element.comments[id] = false;
}
}); });
}); });
}; };