Skip to content

Commit

Permalink
Review Search Box
Browse files Browse the repository at this point in the history
Logic to search codeLines
  • Loading branch information
chidozieononiwu committed Jan 8, 2025
1 parent 4674a2b commit f667188
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
/*----Shadows and Glows------------------------------------------*/
--outer-glow: #{tint-color($primary, 80%)};
--shadow-color: rgba(#{$black}, .15);
--shadow-color-primary: 0 0 0 0.15rem #{rgba($primary, 0.5)};
--box-shadow: 0 .5rem 1rem #{rgba($black, .15)};
--box-shadow-sm: 0 .125rem .25rem #{rgba($black, .075)};
--box-shadow-lg: 0 1rem 3rem #{rgba($black, .175)};
Expand Down Expand Up @@ -120,6 +121,7 @@
/*----Shadows and Glows------------------------------------------*/
--outer-glow: #{tint-color($primary-color, 80%)};
--shadow-color: rgba(#{$white}, .15);
--shadow-color-primary: 0 0 0 0.15rem #{rgba($primary-color, 0.5)};
--box-shadow: 0 .5rem 1rem #{rgba($white, .15)};
--box-shadow-sm: 0 .125rem .25rem #{rgba($white, .075)};
--box-shadow-lg: 0 1rem 3rem #{rgba($white, .175)};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div *ngIf="isLoading" class="spinner-border m-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div *ngIf="codePanelRowSource;" class="viewport {{languageSafeName!}}" (click)="onCodePanelItemClick($event)">
<div *ngIf="codePanelRowSource;" id="viewport" class="{{languageSafeName!}}" (click)="onCodePanelItemClick($event)">
<p-messages class="sticky-top" *ngIf="showNoDiffInContentMessage()" [(value)]="noDiffInContentMessage" [closable]="false" />
<div *uiScroll="let item of codePanelRowSource; let index = index" class="code-line" [attr.data-node-id]="item.nodeIdHashed"
[attr.data-row-position-in-group]="item.rowPositionInGroup" [attr.data-row-type]="item.type" [ngClass]="getRowClassObject(item)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
line-height: 1.5;
display: block;

.viewport {
#viewport {
overflow: auto;
height: calc(100vh - 132px);
will-change: scroll-position, contents;
Expand Down Expand Up @@ -219,6 +219,12 @@
text-decoration: line-through;
}

.codeline-search-match-highlight {
background-color: var(--primary-color);
color: var(--primary-btn-color);
padding: 0px;
margin: 0px;
}

.java {
.javadoc {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { take, takeUntil } from 'rxjs/operators';
import { Datasource, IDatasource, SizeStrategy } from 'ngx-ui-scroll';
import { CommentsService } from 'src/app/_services/comments/comments.service';
Expand All @@ -16,6 +16,7 @@ import { SignalRService } from 'src/app/_services/signal-r/signal-r.service';
import { Subject } from 'rxjs';
import { CommentThreadUpdateAction, CommentUpdatesDto } from 'src/app/_dtos/commentThreadUpdateDto';
import { Menu } from 'primeng/menu';
import { CodeLineSearchInfo } from 'src/app/_models/codeLineSearchInfo';

@Component({
selector: 'app-code-panel',
Expand All @@ -35,8 +36,12 @@ export class CodePanelComponent implements OnChanges{
@Input() userProfile : UserProfile | undefined;
@Input() showLineNumbers: boolean = true;
@Input() loadFailed : boolean = false;
@Input() codeLineSearchText: string | undefined;
@Input() codeLineNavigationDirection: number | undefined;

@Output() hasActiveConversationEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() codeLineSearchInfoEmitter : EventEmitter<CodeLineSearchInfo> = new EventEmitter<CodeLineSearchInfo>();

@ViewChildren(Menu) menus!: QueryList<Menu>;

noDiffInContentMessage : Message[] = [{ severity: 'info', icon:'bi bi-info-circle', detail: 'There is no difference between the two API revisions.' }];
Expand All @@ -48,6 +53,8 @@ export class CodePanelComponent implements OnChanges{
codePanelRowSource: IDatasource<CodePanelRowData> | undefined;
CodePanelRowDatatype = CodePanelRowDatatype;

searchMatchedRowInfo: Map<string, RegExpMatchArray[]> = new Map<string, RegExpMatchArray[]>();

destroy$ = new Subject<void>();

commentThreadNavaigationPointer: number | undefined = undefined;
Expand All @@ -56,7 +63,8 @@ export class CodePanelComponent implements OnChanges{
menuItemsLineActions: MenuItem[] = [];

constructor(private changeDetectorRef: ChangeDetectorRef, private commentsService: CommentsService,
private signalRService: SignalRService, private route: ActivatedRoute, private router: Router, private messageService: MessageService) { }
private signalRService: SignalRService, private route: ActivatedRoute, private router: Router,
private messageService: MessageService, private elementRef: ElementRef<HTMLElement>) { }

ngOnInit() {
this.codeWindowHeight = `${window.innerHeight - 80}`;
Expand Down Expand Up @@ -86,6 +94,17 @@ export class CodePanelComponent implements OnChanges{
if (changes['loadFailed'] && changes['loadFailed'].currentValue) {
this.isLoading = false;
}

if (changes['codeLineSearchText']) {
this.searchCodePanelRowData(this.codeLineSearchText!);
}

if (changes['codeLineNavigationDirection']) {
this.navigateToCodeLineWithSearchMatch(
changes['codeLineNavigationDirection'].previousValue,
changes['codeLineNavigationDirection'].currentValue
);
}
}

onCodePanelItemClick(event: Event) {
Expand Down Expand Up @@ -677,6 +696,97 @@ export class CodePanelComponent implements OnChanges{
const codeLineText = convertRowOfTokensToString(codeLine.rowOfTokens);
navigator.clipboard.writeText(codeLineText);
}

private searchCodePanelRowData(searchText: string) {
this.searchMatchedRowInfo.clear();
if (!searchText || searchText.length === 0) {
this.clearSearchMatchHighlights();
return;
}

let totalMatches = 0;
let matchedNodeIds = new Set<string>();
this.codePanelRowData.forEach((row) => {
if (row.rowOfTokens && row.rowOfTokens.length > 0) {
let codeLineInfo = convertRowOfTokensToString(row.rowOfTokens);
const regex = new RegExp(searchText, "gi");
const matches = [...codeLineInfo.matchAll(regex)];
if (matches.length > 0) {
totalMatches += matches.length;
matchedNodeIds.add(row.nodeIdHashed!);
this.searchMatchedRowInfo.set(row.nodeIdHashed!, matches);
}
}
});
this.highlightSearchMatches();
this.codeLineSearchInfoEmitter.emit({
current: 0,
total: totalMatches,
matchedNodeIds: matchedNodeIds
});
}

private highlightSearchMatches() {
this.clearSearchMatchHighlights();
const codeLines = this.elementRef.nativeElement.querySelectorAll('.code-line');

codeLines.forEach((codeLine) => {
const nodeIdhashed = codeLine.getAttribute('data-node-id');
if (nodeIdhashed && this.searchMatchedRowInfo.has(nodeIdhashed)) {
const tokens = codeLine.querySelectorAll('.code-line-content > span');
const matches = this.searchMatchedRowInfo.get(nodeIdhashed)!;

matches.forEach((match) => {
const matchStartIndex = match.index!;
const matchEndIndex = matchStartIndex + match[0].length;

let currentOffset = 0;
tokens.forEach((token) => {
const tokenText = token.textContent || '';
const tokenLength = tokenText.length;

const tokenStart = currentOffset;
const tokenEnd = currentOffset + tokenLength;

if (matchStartIndex < tokenEnd && matchEndIndex > tokenStart) {
const highlightStart = Math.max(0, matchStartIndex - tokenStart);
const highlightEnd = Math.min(tokenLength, matchEndIndex - tokenStart);

const beforeMatch = tokenText.slice(0, highlightStart);
const matchText = tokenText.slice(highlightStart, highlightEnd);
const afterMatch = tokenText.slice(highlightEnd);

token.innerHTML = `${beforeMatch}<mark class="codeline-search-match-highlight">${matchText}</mark>${afterMatch}`;
}

currentOffset += tokenLength;
});
});
}
});
}

/**
* Navigates to the next or previous code line that contains a search match but is outside the viewport
*/
private navigateToCodeLineWithSearchMatch(previousPosition: number, newPosition: number) {
if (newPosition > previousPosition) {
// Find the next code line that contains a search match but is below the visible viewport
return;
} else if (newPosition < previousPosition) {
// Find the previous code line that contains a search match but is above the visible viewport
return;
}
}

private clearSearchMatchHighlights() {
this.elementRef.nativeElement.querySelectorAll('.codeline-search-match-highlight').forEach((element) => {
const parent = element.parentNode as HTMLElement;
if (parent) {
parent.innerHTML = parent.textContent || '';
}
});
}

private findNextCommentThread (index: number) : CodePanelRowData | undefined {
while (index < this.codePanelRowData.length) {
Expand Down Expand Up @@ -743,15 +853,22 @@ export class CodePanelComponent implements OnChanges{
this.hasActiveConversationEmitter.emit(hasActiveConversation);
}

private loadCodePanelViewPort() {
private loadCodePanelViewPort() {
this.setMaxLineNumberWidth();
this.initializeDataSource().then(() => {
this.codePanelRowSource?.adapter?.init$.pipe(take(1)).subscribe(() => {
this.isLoading = false;
setTimeout(() => {
this.scrollToNode(undefined, this.scrollToNodeId);
const viewport = this.elementRef.nativeElement.ownerDocument.getElementById('viewport');
if (viewport) {
viewport.addEventListener('scroll', (event) => {
if (this.codeLineSearchText && this.codeLineSearchText.length > 0) {
this.highlightSearchMatches();
}
});
}
}, 500);

});
}).catch((error) => {
console.error(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@

<app-page-options-section *ngIf="showCommentsSwitch || (isDiffView && contentHasDiff)" sectionName="Page Utils">
<ul class="list-group">
<li class="list-group-item">
<p-iconField iconPosition="left" class="w-full">
<p-inputIcon styleClass="pi pi-search" />
<input type="text" [formControl]="codeLineSearchText" pInputText placeholder="Search Review" style="width: 100%; box-sizing: border-box;"/>
</p-iconField>
<div *ngIf="codeLineSearchText.value.length > 0" class="d-flex justify-content-between">
<small class="review-search-info">{{ codeLineSearchInfo.current }} of {{ codeLineSearchInfo.total }}</small>
<span class="mt-2">
<i class="bi bi-arrow-up-short border rounded-start review-search-icon" (click)="navigateSearch(-1)"></i>
<i class="bi bi-arrow-down-short border review-search-icon" (click)="navigateSearch(1)"></i>
<i class="bi bi-x border rounded-end review-search-icon" (click)="clearReviewSearch()"></i>
</span>
</div>
</li>
<li class="list-group-item" *ngIf="showCommentsSwitch">
<label class="small mx-1 fw-semibold">Comment:</label>
<div class="d-grid gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,13 @@
color: yellow;
}
}

.review-search-icon {
padding-inline-start: 0.2rem;
padding-inline-end: 0.2rem;
}

.review-search-info {
padding-top: 0.55rem;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import { Review } from 'src/app/_models/review';
import { APIRevision } from 'src/app/_models/revision';
import { ConfigService } from 'src/app/_services/config/config.service';
import { APIRevisionsService } from 'src/app/_services/revisions/revisions.service';
import { take } from 'rxjs';
import { debounceTime, distinctUntilChanged, Subject, take, takeUntil } from 'rxjs';
import { UserProfile } from 'src/app/_models/userProfile';
import { PullRequestsService } from 'src/app/_services/pull-requests/pull-requests.service';
import { PullRequestModel } from 'src/app/_models/pullRequestModel';
import { MenuItemCommandEvent } from 'primeng/api';
import { FormControl } from '@angular/forms';
import { CodeLineSearchInfo } from 'src/app/_models/codeLineSearchInfo';

@Component({
selector: 'app-review-page-options',
templateUrl: './review-page-options.component.html',
styleUrls: ['./review-page-options.component.scss']
})
export class ReviewPageOptionsComponent implements OnInit, OnChanges{
export class ReviewPageOptionsComponent implements OnInit, OnChanges {
@Input() userProfile: UserProfile | undefined;
@Input() isDiffView: boolean = false;
@Input() contentHasDiff: boolean | undefined = false;
Expand All @@ -31,7 +32,8 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
@Input() hasActiveConversation : boolean = false;
@Input() hasHiddenAPIs : boolean = false;
@Input() hasHiddenAPIThatIsDiff : boolean = false;

@Input() codeLineSearchInfo : CodeLineSearchInfo = new CodeLineSearchInfo();

@Output() diffStyleEmitter : EventEmitter<string> = new EventEmitter<string>();
@Output() showCommentsEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() showSystemCommentsEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
Expand All @@ -47,7 +49,11 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
@Output() commentThreadNavaigationEmitter : EventEmitter<CodeLineRowNavigationDirection> = new EventEmitter<CodeLineRowNavigationDirection>();
@Output() diffNavaigationEmitter : EventEmitter<CodeLineRowNavigationDirection> = new EventEmitter<CodeLineRowNavigationDirection>();
@Output() copyReviewTextEmitter : EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() codeLineSearchTextEmitter : EventEmitter<string> = new EventEmitter<string>();
@Output() codeLineSearchNaviationEmmiter : EventEmitter<number> = new EventEmitter<number>();

private destroy$ = new Subject<void>();

webAppUrl : string = this.configService.webAppUrl

showCommentsSwitch : boolean = true;
Expand Down Expand Up @@ -75,10 +81,14 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
reviewApprover: string = 'azure-sdk';
copyReviewTextButtonText : string = 'Copy review text';

codeLineSearchText: FormControl = new FormControl('');

associatedPullRequests : PullRequestModel[] = [];
pullRequestsOfAssociatedAPIRevisions : PullRequestModel[] = [];
CodeLineRowNavigationDirection = CodeLineRowNavigationDirection;

codeLineSearchNavigationPosition : number = 0;

//Approvers Options
selectedApprovers: string[] = [];

Expand Down Expand Up @@ -110,6 +120,14 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
this.activeAPIRevision?.assignedReviewers.map(revision => this.selectedApprovers.push(revision.assingedTo));
this.setAPIRevisionApprovalStates();
this.setReviewApprovalStatus();

this.codeLineSearchText.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe((searchText: string) => {
this.codeLineSearchTextEmitter.emit(searchText);
});
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -233,7 +251,6 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
this.showHiddenAPIEmitter.emit(event.checked);
}


handleAssignedReviewersChange() {

const existingApprovers = new Set(this.activeAPIRevision!.assignedReviewers.map(reviewer => reviewer.assingedTo));
Expand Down Expand Up @@ -336,6 +353,23 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
this.copyReviewTextEmitter.emit(true);
}

clearReviewSearch() {
this.codeLineSearchText.setValue('');
}

navigateCommentThread(direction: CodeLineRowNavigationDirection) {
this.commentThreadNavaigationEmitter.emit(direction);
}

/**
* Use positive number to navigate to the next search result and negative number to navigate to the previous search result
* @param number
*/
navigateSearch(number: number) {
this.codeLineSearchNavigationPosition += number;
this.codeLineSearchNaviationEmmiter.emit(this.codeLineSearchNavigationPosition);
}

handleAPIRevisionApprovalAction() {
if (!this.activeAPIRevisionIsApprovedByCurrentUser && (this.hasActiveConversation || this.hasFatalDiagnostics)) {
this.showAPIRevisionApprovalModal = true;
Expand Down Expand Up @@ -364,4 +398,10 @@ export class ReviewPageOptionsComponent implements OnInit, OnChanges{
let newQueryParams = getQueryParams(this.route); // this automatically excludes the nId query parameter
this.router.navigate([], { queryParams: newQueryParams, state: { skipStateUpdate: true } });
}


ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Loading

0 comments on commit f667188

Please sign in to comment.