diff --git a/public/imgs/only-in-selection.png b/public/imgs/only-in-selection.png new file mode 100644 index 0000000..3c35fc3 Binary files /dev/null and b/public/imgs/only-in-selection.png differ diff --git a/public/imgs/whole-word.png b/public/imgs/whole-word.png new file mode 100644 index 0000000..d147682 Binary files /dev/null and b/public/imgs/whole-word.png differ diff --git a/src/app/app.component.html b/src/app/app.component.html index b513172..e196981 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0a1cf7c..7d8610b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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', diff --git a/src/app/common/services/editor/search-replace/search-replace.service.ts b/src/app/common/services/editor/search-replace/search-replace.service.ts new file mode 100644 index 0000000..1e74c10 --- /dev/null +++ b/src/app/common/services/editor/search-replace/search-replace.service.ts @@ -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 = new ReplaySubject(1); + + + constructor() { + } + + + public sendMessage(data: ServiceMessage): void { + this.messageSubject.next(data); + } + + public getMessage$(): Observable { + return this.messageSubject.asObservable(); + } + +} \ No newline at end of file diff --git a/src/app/common/services/editor/files.service.ts b/src/app/common/services/files.service.ts similarity index 94% rename from src/app/common/services/editor/files.service.ts rename to src/app/common/services/files.service.ts index fe84b7b..ea2997e 100644 --- a/src/app/common/services/editor/files.service.ts +++ b/src/app/common/services/files.service.ts @@ -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'; diff --git a/src/app/editor/code-view/view.base.ts b/src/app/editor/code-view/view.base.ts index 7a4265f..fa7ced1 100644 --- a/src/app/editor/code-view/view.base.ts +++ b/src/app/editor/code-view/view.base.ts @@ -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() { diff --git a/src/app/editor/code-view/view.component.ts b/src/app/editor/code-view/view.component.ts index c10d5b1..44ed6d2 100644 --- a/src/app/editor/code-view/view.component.ts +++ b/src/app/editor/code-view/view.component.ts @@ -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(); }); diff --git a/src/app/editor/editors.component.ts b/src/app/editor/editors.component.ts index 926cfde..f6b0b5e 100644 --- a/src/app/editor/editors.component.ts +++ b/src/app/editor/editors.component.ts @@ -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"; diff --git a/src/app/editor/search-replace/search-replace.component.css b/src/app/editor/search-replace/search-replace.component.css new file mode 100644 index 0000000..a98271c --- /dev/null +++ b/src/app/editor/search-replace/search-replace.component.css @@ -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; +} \ No newline at end of file diff --git a/src/app/editor/search-replace/search-replace.component.html b/src/app/editor/search-replace/search-replace.component.html new file mode 100644 index 0000000..45acd09 --- /dev/null +++ b/src/app/editor/search-replace/search-replace.component.html @@ -0,0 +1,84 @@ +
+
+
+ +
+ +
+ +
+ +
+ + + + + +
+
+ +
+ +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/src/app/editor/search-replace/search-replace.component.ts b/src/app/editor/search-replace/search-replace.component.ts new file mode 100644 index 0000000..4568c8f --- /dev/null +++ b/src/app/editor/search-replace/search-replace.component.ts @@ -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 = 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); + } + +} \ No newline at end of file diff --git a/src/assets/css/styles.css b/src/assets/css/styles.css index e3db070..5b18a1b 100644 --- a/src/assets/css/styles.css +++ b/src/assets/css/styles.css @@ -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;