Wiring of search-replace popup; moved some services up one level
This commit is contained in:
parent
5aa6c7ca10
commit
e64a18b18b
BIN
public/imgs/only-in-selection.png
Normal file
BIN
public/imgs/only-in-selection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
public/imgs/whole-word.png
Normal file
BIN
public/imgs/whole-word.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
@ -2,6 +2,7 @@
|
||||
<info-bar></info-bar>
|
||||
<tabs></tabs>
|
||||
<editors></editors>
|
||||
<search-replace></search-replace>
|
||||
|
||||
<files-modal></files-modal>
|
||||
</div>
|
@ -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',
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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';
|
||||
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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";
|
||||
|
||||
|
34
src/app/editor/search-replace/search-replace.component.css
Normal file
34
src/app/editor/search-replace/search-replace.component.css
Normal 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;
|
||||
}
|
84
src/app/editor/search-replace/search-replace.component.html
Normal file
84
src/app/editor/search-replace/search-replace.component.html
Normal 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>
|
268
src/app/editor/search-replace/search-replace.component.ts
Normal file
268
src/app/editor/search-replace/search-replace.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user