Wiring of search-replace popup; moved some services up one level

This commit is contained in:
itdominator 2025-07-03 22:46:19 -05:00
parent 5aa6c7ca10
commit e64a18b18b
13 changed files with 457 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/imgs/whole-word.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -2,6 +2,7 @@
<info-bar></info-bar>
<tabs></tabs>
<editors></editors>
<search-replace></search-replace>
<files-modal></files-modal>
</div>

View File

@ -3,6 +3,7 @@ import { Component } from '@angular/core';
import { InfoBarComponent } from './editor/info-bar/info-bar.component';
import { TabsComponent } from './editor/tabs/tabs.component';
import { EditorsComponent } from './editor/editors.component';
import { SearchReplaceComponent } from "./editor/search-replace/search-replace.component";
import { FilesModalComponent } from "./common/components/modals/files/files-modal.component";
@ -13,6 +14,7 @@ import { FilesModalComponent } from "./common/components/modals/files/files-moda
InfoBarComponent,
TabsComponent,
EditorsComponent,
SearchReplaceComponent,
FilesModalComponent
],
templateUrl: './app.component.html',

View File

@ -0,0 +1,27 @@
import { Injectable, inject } from '@angular/core';
import { ReplaySubject, Observable } from 'rxjs';
import { ServiceMessage } from '../../../types/service-message.type';
@Injectable({
providedIn: 'root'
})
export class SearchReplaceService {
private messageSubject: ReplaySubject<ServiceMessage> = new ReplaySubject<ServiceMessage>(1);
constructor() {
}
public sendMessage(data: ServiceMessage): void {
this.messageSubject.next(data);
}
public getMessage$(): Observable<ServiceMessage> {
return this.messageSubject.asObservable();
}
}

View File

@ -4,10 +4,10 @@ import { ReplaySubject, Observable } from 'rxjs';
import { EditSession, UndoManager } from 'ace-builds';
import { getModeForPath } from 'ace-builds/src-noconflict/ext-modelist';
import { TabsService } from './tabs/tabs.service';
import { TabsService } from './editor/tabs/tabs.service';
import { NewtonFile } from '../../types/file.type';
import { ServiceMessage } from '../../types/service-message.type';
import { NewtonFile } from '../types/file.type';
import { ServiceMessage } from '../types/service-message.type';

View File

@ -5,7 +5,8 @@ import { InfoBarService } from '../../common/services/editor/info-bar/info-bar.s
import { FilesModalService } from '../../common/services/editor/modals/files-modal.service';
import { TabsService } from '../../common/services/editor/tabs/tabs.service';
import { EditorsService } from '../../common/services/editor/editors.service';
import { FilesService } from '../../common/services/editor/files.service';
import { FilesService } from '../../common/services/files.service';
import { SearchReplaceService } from '../../common/services/editor/search-replace/search-replace.service';
import { EditorSettings } from "../../common/configs/editor.config";
import { NewtonFile } from '../../common/types/file.type';
@ -16,17 +17,18 @@ import { ServiceMessage } from '../../common/types/service-message.type';
@Directive()
export class CodeViewBase {
public uuid: string = uuid.v4();
@Input() public isDefault: boolean = false;
@Input() public isMiniMap: boolean = false;
public uuid: string = uuid.v4();
@Input() public isDefault: boolean = false;
@Input() public isMiniMap: boolean = false;
public leftSiblingUUID!: string;
public rightSiblingUUID!: string;
protected infoBarService: InfoBarService = inject(InfoBarService);
protected filesModalService: FilesModalService = inject(FilesModalService);
protected tabsService: TabsService = inject(TabsService);
protected editorsService: EditorsService = inject(EditorsService);
protected filesService: FilesService = inject(FilesService);
protected infoBarService: InfoBarService = inject(InfoBarService);
protected filesModalService: FilesModalService = inject(FilesModalService);
protected tabsService: TabsService = inject(TabsService);
protected editorsService: EditorsService = inject(EditorsService);
protected filesService: FilesService = inject(FilesService);
protected searchReplaceService: SearchReplaceService = inject(SearchReplaceService);
@ViewChild('editor') editorElm!: ElementRef;
@Input() editorSettings!: typeof EditorSettings;
@ -94,11 +96,19 @@ export class CodeViewBase {
}
public searchPopup() {
this.editor.execCommand("find");
let message = new ServiceMessage();
message.action = "toggle-search-replace";
this.searchReplaceService.sendMessage(message);
// this.editor.execCommand("find");
}
public replacePopup() {
this.editor.execCommand("replace");
let message = new ServiceMessage();
message.action = "toggle-search-replace";
this.searchReplaceService.sendMessage(message);
// this.editor.execCommand("replace");
}
public showFilesList() {

View File

@ -7,10 +7,10 @@ import "ace-builds/src-noconflict/ext-keybinding_menu";
import "ace-builds/src-noconflict/ext-command_bar";
import "ace-builds/src-noconflict/ext-prompt";
import "ace-builds/src-noconflict/ext-code_lens";
import "ace-builds/src-noconflict/ext-searchbox";
// import "ace-builds/src-noconflict/ext-searchbox";
import "ace-builds/src-noconflict/ext-language_tools";
//import "ace-builds/src-noconflict/theme-one_dark";
//import "ace-builds/src-noconflict/theme-penguins_in_space";
// import "ace-builds/src-noconflict/theme-one_dark";
// import "ace-builds/src-noconflict/theme-penguins_in_space";
import "ace-builds/src-noconflict/theme-gruvbox";
import { CodeViewBase } from './view.base';
@ -51,6 +51,7 @@ export class CodeViewComponent extends CodeViewBase {
if (this.isDefault) {
this.editorsService.setActiveEditor(this.uuid);
this.addActiveStyling();
this.editor.focus();
}
if (this.isMiniMap) {
@ -111,8 +112,10 @@ export class CodeViewComponent extends CodeViewBase {
let message = new ServiceMessage();
message.action = "set-active-editor";
message.editorUUID = this.uuid;
message.rawData = this.editor;
this.editorsService.sendMessage(message);
this.searchReplaceService.sendMessage(message);
this.updateInfoBar();
});

View File

@ -3,7 +3,7 @@ import { Subject, takeUntil } from 'rxjs';
import { EditorsService } from '../common/services/editor/editors.service';
import { TabsService } from '../common/services/editor/tabs/tabs.service';
import { FilesService } from '../common/services/editor/files.service';
import { FilesService } from '../common/services/files.service';
import { CodeViewComponent } from "./code-view/view.component";

View File

@ -0,0 +1,34 @@
.width-8em {
width: 8em;
}
.margin-tb-1em {
margin-top: 1em;
margin-bottom: 1em;
}
.selected {
background-color: rgba(125, 125, 125, 1);
color: rgba(0, 0, 0, 1);
}
.searching,
.search-success,
.search-fail {
border-style: solid;
color: rgba(125, 125, 125, 1) !important;
}
.searching {
border-color: rgba(0, 225, 225, 0.64) !important;
}
.search-success {
background: rgba(136, 204, 39, 0.12) !important;
border-color: rgba(136, 204, 39, 1) !important;
}
.search-fail {
background: rgba(170, 18, 18, 0.12) !important;
border-color: rgba(200, 18, 18, 1) !important;
}

View File

@ -0,0 +1,84 @@
<div class="col">
<div class="row">
<div class="col col-3">
<label id="find-status-lbl">Find in Current File</label>
</div>
<div class="col col-4">
<label id="find-options-lbl">Finding with Options: {{findOptions || "Case Insensitive"}}</label>
</div>
<div class="col col-5 line-height-32px">
<button title="Close Panel"
class="float-end btn btn-sm btn-dark"
(click)="hideSearchReplace()">X
</button>
<button id="whole-word-btn" title="Whole Word"
class="float-end btn btn-sm btn-dark"
(click)="toggleWholeWordSearch($event)">
<img src="resources/imgs/whole-word.png" />
</button>
<button id="only-in-selection-btn" title="Only In Selection"
class="float-end btn btn-sm btn-dark"
(click)="toggleSelectionOnlyScan($event)">
<img src="resources/imgs/only-in-selection.png" />
</button>
<button id="match-case-btn" title="Match Case"
class="float-end btn btn-sm btn-dark"
(click)="toggleCaseSensitive($event)">Aa
</button>
<button id="use-regex-btn" title="Use Regex"
class="float-end btn btn-sm btn-dark"
(click)="toggleRegex($event)">.*
</button>
</div>
</div>
<div class="margin-tb-1em"></div>
<div class="row">
<div class="col">
<div class="row">
<div class="col">
<div class="input-group-sm mb-3">
<input #findEntryElm
id="find-entry"
class="form-control"
type="search"
(keyup)="searchForString()"
placeholder="Find in current file..."
aria-label="Find in current file..."
/>
</div>
</div>
<div class="col col-auto">
<button id="find-btn" class="width-8em btn btn-sm btn-dark" (click)="findNextEntry()">Find</button>
<button id="find-all-btn" class="width-8em btn btn-sm btn-dark" (click)="findAllEntries()">Find All</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="row">
<div class="col">
<div class="input-group-sm mb-3">
<input #replaceEntryElm
id="replace-entry"
class="form-control"
type="search"
(keyup)="replaceEntry($event)"
title="Replace in current file..."
placeholder="Replace in current file..."
/>
</div>
</div>
<div class="col col-auto">
<button id="replace-btn" class="width-8em btn btn-sm btn-dark" (click)="replaceEntry($event)">Replace</button>
<button id="replace-all-btn" class="width-8em btn btn-sm btn-dark" (click)="replaceAll()">Replace All</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,268 @@
import { Component, ElementRef, HostBinding, Input, ViewChild, inject } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { SearchReplaceService } from '../../common/services/editor/search-replace/search-replace.service';
import { ServiceMessage } from '../../common/types/service-message.type';
@Component({
selector: 'search-replace',
standalone: true,
imports: [
],
templateUrl: './search-replace.component.html',
styleUrl: './search-replace.component.css',
host: {
'class': 'row search-replace',
"(keyup)": "globalSearchReplaceKeyHandler($event)"
}
})
export class SearchReplaceComponent {
private unsubscribe: Subject<void> = new Subject();
private searchReplaceService: SearchReplaceService = inject(SearchReplaceService);
@HostBinding("class.hidden") isHidden: boolean = true;
@ViewChild('findEntryElm') findEntryElm!: ElementRef;
@ViewChild('replaceEntryElm') replaceEntryElm!: ElementRef;
private editor!: any;
@Input() findOptions: string = "";
private useWholeWordSearch: boolean = false;
private searchOnlyInSelection: boolean = false;
private useCaseSensitive: boolean = false;
private useRegex: boolean = false;
private selection: string = "";
private query: string = "";
private toStr: string = "";
private isBackwards: boolean = false;
private isWrap: boolean = true;
private searchTimeoutId: number = -1;
private searchTimeout: number = 400;
constructor() {
}
private ngAfterViewInit(): void {
this.loadSubscribers();
}
private ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
private loadSubscribers() {
this.searchReplaceService.getMessage$().pipe(
takeUntil(this.unsubscribe)
).subscribe((message: ServiceMessage) => {
if (message.action === "toggle-search-replace") {
this.toggleSearchReplace(message);
} else if (message.action === "set-active-editor") {
this.setActiveEditor(message);
}
});
}
private toggleSearchReplace(message: ServiceMessage) {
this.selection = this.editor.getSelectedText();
this.findEntryElm.nativeElement.value = this.selection;
if (this.selection && !this.isHidden) {
this.findEntryElm.nativeElement.focus();
return;
}
this.isHidden = !this.isHidden;
if (this.isHidden) {
this.editor.focus();
return;
}
setTimeout(() => {
this.findEntryElm.nativeElement.focus();
}, 200);
}
private setActiveEditor(message: ServiceMessage) {
if (!this.isHidden && this.editor == message.rawData) return;
this.editor = message.rawData;
if (this.isHidden) return;
this.searchForString();
}
public hideSearchReplace() {
if (this.selection) {
this.selection = "";
return;
}
this.isHidden = true;
this.editor.focus();
}
public globalSearchReplaceKeyHandler(event: any) {
if (event.ctrlKey && event.key === "f") {
this.hideSearchReplace();
} else if (event.ctrlKey && event.key === "l") {
this.findEntryElm.nativeElement.focus();
} else if (event.ctrlKey && event.key === "r") {
this.replaceEntryElm.nativeElement.focus();
}
}
public toggleWholeWordSearch(event: any) {
let target = event.target;
if (target.nodeName === "IMG")
target = target.parentElement;
this.useWholeWordSearch = !this.useWholeWordSearch;
target.classList.toggle("selected");
this.setFindOptionsLbl();
this.findAllEntries();
}
public toggleSelectionOnlyScan(event: any) {
let target = event.target;
if (target.nodeName === "IMG")
target = target.parentElement;
this.searchOnlyInSelection = !this.searchOnlyInSelection;
target.classList.toggle("selected");
this.setFindOptionsLbl();
this.findAllEntries();
}
public toggleCaseSensitive(event: any) {
this.useCaseSensitive = !this.useCaseSensitive;
event.target.classList.toggle("selected");
this.setFindOptionsLbl();
this.findAllEntries();
}
public toggleRegex(event: any) {
this.useRegex = !this.useRegex;
event.target.classList.toggle("selected");
this.setFindOptionsLbl();
this.findAllEntries();
}
private setFindOptionsLbl() {
let findOptionsStr = "";
if (this.useRegex)
findOptionsStr += "Regex"
findOptionsStr += (findOptionsStr) ? ", " : "";
findOptionsStr += (this.useCaseSensitive) ? "Case Sensitive" : "Case InSensitive";
if (this.searchOnlyInSelection)
findOptionsStr += ", Within Current Selection"
if (this.useWholeWordSearch)
findOptionsStr += ", Whole Word"
this.findOptions = findOptionsStr;
}
public findNextEntry() {
this.editor.findNext();
}
public findAllEntries() {
this.query = this.findEntryElm.nativeElement.value;
if (!this.query) return;
let totalCount = this.editor.findAll(this.query, {
backwards: this.isBackwards,
wrap: this.isWrap,
caseSensitive: this.useCaseSensitive,
wholeWord: this.useWholeWordSearch,
regExp: this.useRegex,
range: this.searchOnlyInSelection
});
}
public findPreviousEntry() {
this.editor.findPrevious();
}
public replaceEntry(event: any) {
if (event instanceof KeyboardEvent) {
if (event.key !== "Enter") {
return;
}
}
let fromStr = this.findEntryElm.nativeElement.value;
let toStr = this.replaceEntryElm.nativeElement.value;
if (!fromStr || !toStr) return;
let totalCount = this.editor.replace(toStr, fromStr, {
backwards: this.isBackwards,
wrap: this.isWrap,
caseSensitive: this.useCaseSensitive,
wholeWord: this.useWholeWordSearch,
regExp: this.useRegex,
range: this.searchOnlyInSelection
});
this.editor.clearSelection();
this.editor.findNext();
}
public replaceAll() {
let fromStr = this.findEntryElm.nativeElement.value;
let toStr = this.replaceEntryElm.nativeElement.value;
if (!fromStr || !toStr) return;
let totalCount = this.editor.replaceAll(toStr, fromStr, {
backwards: this.isBackwards,
wrap: this.isWrap,
caseSensitive: this.useCaseSensitive,
wholeWord: this.useWholeWordSearch,
regExp: this.useRegex,
range: this.searchOnlyInSelection
});
}
public searchForString() {
if (event instanceof KeyboardEvent) {
if (event.key !== "Enter") {
return;
}
}
this.query = this.findEntryElm.nativeElement.value;
if (!this.query) return;
if (this.searchTimeoutId) { clearTimeout(this.searchTimeoutId); }
this.searchTimeoutId = setTimeout(() => {
let totalCount = this.editor.find(this.query, {
backwards: this.isBackwards,
wrap: this.isWrap,
caseSensitive: this.useCaseSensitive,
wholeWord: this.useWholeWordSearch,
regExp: this.useRegex,
range: this.searchOnlyInSelection
});
}, this.searchTimeout);
}
}

View File

@ -26,6 +26,16 @@ body {
text-align: center;
}
.search-replace {
bottom: 2em;
z-index: 999;
display: inline-block;
position: fixed;
width: 100%;
background-color: rgba(64, 64, 64, 0.24);
}
.tabs {
display: flex;
overflow: auto;