Compare commits

...

11 Commits

42 changed files with 999 additions and 258 deletions

View File

@@ -34,7 +34,7 @@
},
{
"glob":"**/*",
"input":"node_modules/ace-builds/src-noconflict",
"input":"node_modules/ace-builds/src-min-noconflict",
"output":"ace"
}
],
@@ -44,9 +44,11 @@
"node_modules/bootstrap/scss/bootstrap.scss",
"node_modules/bootstrap-icons/font/bootstrap-icons.css",
"src/assets/css/styles.css",
"src/assets/css/overrides.css"
"src/assets/css/ace-overrides.css"
],
"scripts":[
"src/libs/showdown.min.js"
],
"optimization": true
},
@@ -64,7 +66,7 @@
"maximumError":"8kB"
}
],
"optimization":false
"optimization": true
},
"development":{
"outputHashing": "all",
@@ -110,7 +112,6 @@
"src/styles.css"
],
"scripts":[
]
}
}

View File

@@ -100,6 +100,26 @@ const saveFileAs = () => {
});
}
const chooseFolder = () => {
return dialog.showOpenDialog(
{
title: "Choose Folder:",
defaultPath: HOME_DIR,
properties: [
'openDirectory'
]
}
).then((response) => {
if (response.canceled) {
console.debug("Canceled folder selection...");
return "";
}
console.log(response)
return response.filePaths[0];
});
}
const openFiles = (startPath) => {
dialog.showOpenDialog(
{
@@ -171,6 +191,7 @@ const closeFile = (fpath) => {
module.exports = {
newtonFs: {
setWindow: setWindow,
chooseFolder: chooseFolder,
openFiles: openFiles,
saveFile: saveFile,
saveFileAs: saveFileAs,

View File

@@ -72,7 +72,8 @@ const loadHandlers = () => {
ipcMain.handle('openFiles', (eve, startPath) => newton.fs.openFiles(startPath));
ipcMain.handle('saveFile', (eve, path, content) => newton.fs.saveFile(path, content));
ipcMain.handle('closeFile', (eve, path) => newton.fs.closeFile(path));
ipcMain.handle('saveFileAs', (eve, content) => newton.fs.saveFileAs(content));
ipcMain.handle('saveFileAs', (eve) => newton.fs.saveFileAs());
ipcMain.handle('chooseFolder', (eve) => newton.fs.chooseFolder());
}
app.whenReady().then(async () => {

View File

@@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld('fs', {
openFiles: (startPath) => ipcRenderer.invoke("openFiles", startPath),
saveFile: (path, content) => ipcRenderer.invoke("saveFile", path, content),
saveFileAs: () => ipcRenderer.invoke("saveFileAs"),
chooseFolder: () => ipcRenderer.invoke("chooseFolder"),
closeFile: (path) => ipcRenderer.invoke("closeFile", path),
getPathForFile: (file) => webUtils.getPathForFile(file),
onLoadFiles: (callback) => ipcRenderer.on('load-files', (_event, paths) => callback(paths)),

View File

@@ -4,6 +4,8 @@
"version": "0.0.1",
"author": "ITDominator",
"license": "GPL-2.0-only",
"homepage": "https://www.itdominator.com",
"email": "1itdominator@gmail.com",
"main": "newton/main.js",
"private": true,
"scripts": {
@@ -11,6 +13,7 @@
"electron-start": "electron . --trace-warnings --start-as=build",
"electron-pack": "ng build --base-href ./ && electron-builder --dir",
"electron-dist": "ng build --base-href ./ && electron-builder",
"electron-dist-linux": "ng build --base-href ./ && electron-builder --linux deb zip AppImage",
"electron-dist-all": "ng build --base-href ./ && electron-builder -mwl",
"electron-concurrently": "concurrently 'ng serve' 'electron . --trace-warnings --start-as=ng-serve'",
"ng-serve": "ng serve",
@@ -24,7 +27,10 @@
"icon": "./icos/",
"files": [
"newton/",
"build/"
"build/",
"!node_modules/ace-builds/",
"!node_modules/web-streams-polyfill/",
"!node_modules/@angular/"
],
"mac": {
"category": "public.app-category.developer-tools"
@@ -33,8 +39,9 @@
"target": "portable"
},
"linux": {
"target": "AppImage",
"category": "Development"
"target": "zip",
"category": "Development",
"maintainer": "ITDominator"
}
},
"postinstall": "electron-builder install-app-deps",

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,8 @@
<info-bar></info-bar>
<tabs></tabs>
<editors></editors>
<search-replace></search-replace>
<markdown-preview></markdown-preview>
<files-modal></files-modal>
<lsp-manager></lsp-manager>
</div>

View File

@@ -3,7 +3,9 @@ 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 { FilesModalComponent } from "./common/components/modals/files/files-modal.component";
import { SearchReplaceComponent } from "./editor/search-replace/search-replace.component";
import { MarkdownPreviewComponent } from "./editor/markdown-preview/markdown-preview.component";
import { LspManagerComponent } from "./editor/lsp-manager/lsp-manager.component";
@@ -13,7 +15,9 @@ import { FilesModalComponent } from "./common/components/modals/files/files-moda
InfoBarComponent,
TabsComponent,
EditorsComponent,
FilesModalComponent
SearchReplaceComponent,
MarkdownPreviewComponent,
LspManagerComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.css',

View File

@@ -1,16 +0,0 @@
.modal-column {
min-height: 24em;
max-height: 24em;
overflow: auto;
}
.close-button {
background: rgba(116, 0, 0, 0.64);
border-style: solid;
border-color: rgba(0, 0, 0, 0.64);
border-width: 1px;
}
.close-button:hover {
background: rgba(256, 0, 0, 0.64);
}

View File

@@ -1,52 +0,0 @@
<div #filesModal
id="filesModal" class="modal fade" tabindex="-1" role="dialog"
>
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Files:</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col">
<div class="row">
<div class="col modal-column">
<div #filesList *ngFor="let file of files" class="row">
<div class="col-11 title"
title="{{file.path}}"
uuid="{{file.uuid}}"
path="{{file.path}}"
>
{{file.title}}
</div>
<div class="col-1 close-button">
X
</div>
</div>
</div>
</div>
<div class="row">
<input #filesSearch type="text" placeholder="Search..." />
</div>
</div>
<div class="col modal-column">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,85 +0,0 @@
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Subject, takeUntil } from 'rxjs';
import * as bootstrap from "bootstrap";
import { FilesModalService } from "../../../services/editor/modals/files-modal.service";
import { TabsService } from '../../../services/editor/tabs/tabs.service';
import { ServiceMessage } from '../../../types/service-message.type';
@Component({
selector: 'files-modal',
standalone: true,
imports: [
CommonModule
],
templateUrl: './files-modal.component.html',
styleUrl: './files-modal.component.css',
host: {
'class': ''
}
})
export class FilesModalComponent {
private unsubscribe: Subject<void> = new Subject();
private filesModalService: FilesModalService = inject(FilesModalService);
private tabsService: TabsService = inject(TabsService);
filesModal!: bootstrap.Modal;
files: any[] = [];
constructor() {
}
private ngAfterViewInit(): void {
this.loadSubscribers();
}
private loadSubscribers() {
this.tabsService.getMessage$().pipe(
takeUntil(this.unsubscribe)
).subscribe((data: ServiceMessage) => {
if (data.action === "create-tab") {
this.createFileRow(data.fileName, data.fileUUID, data.filePath);
}
});
this.filesModalService.showFilesModalRequested$().pipe(
takeUntil(this.unsubscribe)
).subscribe(() => {
if (!this.filesModal) {
this.createModal();
}
this.showModal();
});
this.filesModalService.addFileToModalRequested$().pipe(
takeUntil(this.unsubscribe)
).subscribe((uuid: string) => {
if (!this.filesModal) {
this.createModal();
}
});
}
private createModal() {
this.filesModal = new bootstrap.Modal("#filesModal", {});
}
public createFileRow(title: string, uuid: string, path: string): void {
this.files.push({title: title, uuid: uuid, path: path})
}
public showModal() {
this.filesModal?.toggle();
}
}

View File

@@ -15,23 +15,23 @@ export const Keybindings: Array<{}> = [
name: "openCommandPalette2",
bindKey: {linux: "command-shift-/|F1", win: "ctrl-shift-/|F1"},
readOnly: false
}, {
name: "showFilesModal",
bindKey: {win: "ctrl-shift-b", mac: "ctrl-shift-b"},
service: "filesModalService",
readOnly: false
}, {
name: "showFilesList",
bindKey: {win: "ctrl-b", mac: "ctrl-b"},
readOnly: false
}, {
name: "showLSPModal",
name: "lspManagerPopup",
bindKey: {win: "ctrl-shift-l", mac: "ctrl-shift-l"},
service: "",
readOnly: false
}, {
name: "markdownPreviewPopup",
bindKey: {win: "ctrl-shift-m", mac: "ctrl-shift-m"},
readOnly: false
}, {
name: "searchPopup",
bindKey: {win: "ctrl-f", mac: "ctrl-f"},
readOnly: true
readOnly: false
}, {
name: "replacePopup",
bindKey: {win: "ctrl-r", mac: "ctrl-r"},

View File

@@ -7,6 +7,7 @@ import { ServiceMessage } from '../../types/service-message.type';
import { EditorSettings } from "../../configs/editor.config";
import { NewtonFile } from '../../types/file.type';
import { EditorType } from '../../types/editor.type';
@@ -38,7 +39,7 @@ export class EditorsService {
}
public set(uuid: string, component: CodeViewComponent) {
if (component.isMiniMap) {
if (component.mode == EditorType.MiniMap) {
this.miniMapView = component;
return;
}

View File

@@ -1,26 +1,30 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
import { ReplaySubject, Observable } from 'rxjs';
import { AceLanguageClient, LanguageClientConfig } from 'ace-linters/build/ace-language-client';
import { LanguageProvider } from "ace-linters";
import { ServiceMessage } from '../../../types/service-message.type';
@Injectable({
providedIn: 'root'
})
export class LSPService {
export class LspManagerService {
private messageSubject: ReplaySubject<ServiceMessage> = new ReplaySubject<ServiceMessage>(1);
lspConfigData!: {};
languageProviders: {} = {};
languageProviders: {} = {};
workspaceFolder: string = "";
constructor() {
this.loadLSPService();
}
private loadLSPService() {
this.getLspConfigData().then((lspConfigData: string) => {
public loadLspConfigData(): Promise<string | void> {
return this.getLspConfigData().then((lspConfigData: string) => {
this.lspConfigData = JSON.parse(lspConfigData);
if (this.lspConfigData["message"]) {
@@ -31,11 +35,13 @@ export class LSPService {
this.lspConfigData = {};
}
return lspConfigData;
});
}
public registerEditor(editor: any): void {
let modeParts = editor.getSession()["$modeId"].split("/");
let modeParts = editor.session.getMode()["$Id"].split("/");
let mode = modeParts[ modeParts.length - 1 ];
if ( !this.languageProviders[mode] ) {
@@ -75,14 +81,26 @@ export class LSPService {
return LanguageProvider.create(worker);
}
protected setSessionFilePath(session: any, mode: string = "", filePath: string = "") => {
if ( !session || !mode || !filePath || !this.languageProviders[mode] ) return;
protected setSessionFilePath(session: any, filePath: string = "") {
if ( !session || !filePath ) return;
let mode = session.getMode()["$Id"];
if ( !this.languageProviders[mode] ) return;
this.languageProviders[mode].setSessionFilePath(session, filePath);
}
protected closeDocument(session: any, mode: string) => {
if ( !session || !mode || !this.languageProviders[mode] ) return;
protected closeDocument(session: any) {
if ( !session ) return;
let mode = session.getMode()["$Id"];
if ( !this.languageProviders[mode] ) return;
this.languageProviders[mode].closeDocument(session);
}
public sendMessage(data: ServiceMessage): void {
this.messageSubject.next(data);
}
public getMessage$(): Observable<ServiceMessage> {
return this.messageSubject.asObservable();
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { ReplaySubject, Observable } from 'rxjs';
import { ServiceMessage } from '../../../types/service-message.type';
@Injectable({
providedIn: 'root'
})
export class MarkdownPreviewService {
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

@@ -0,0 +1,27 @@
import { Injectable } 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

@@ -0,0 +1,5 @@
export abstract class EditorType {
static MiniMap: string = "mini-map";
static ReadOnly: string = "read-only";
static Standalone: string = "standalone";
}

View File

@@ -5,7 +5,10 @@ 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 { MarkdownPreviewService } from '../../common/services/editor/markdown-preview/markdown-preview.service';
import { LspManagerService } from '../../common/services/editor/lsp-manager/lsp-manager.service';
import { EditorSettings } from "../../common/configs/editor.config";
import { NewtonFile } from '../../common/types/file.type';
@@ -16,17 +19,20 @@ 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 mode: string = "";
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);
protected markdownPreviewService: MarkdownPreviewService = inject(MarkdownPreviewService);
protected lspManagerService: LspManagerService = inject(LspManagerService);
@ViewChild('editor') editorElm!: ElementRef;
@Input() editorSettings!: typeof EditorSettings;
@@ -93,12 +99,32 @@ export class CodeViewBase {
this.editor.showKeyboardShortcuts();
}
public lspManagerPopup() {
let message = new ServiceMessage();
message.action = "toggle-lsp-manager";
this.lspManagerService.sendMessage(message);
}
public markdownPreviewPopup() {
let message = new ServiceMessage();
message.action = "toggle-markdown-preview";
this.markdownPreviewService.sendMessage(message);
}
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() {
@@ -159,6 +185,11 @@ export class CodeViewBase {
window.main.toggleFullScreen();
}
public setAsReadOnly() {
this.editor.setReadOnly(true);
this.editor.setShowPrintMargin(false);
}
public setAsMiniMapView() {
this.editor.renderer.showLineNumbers = false;
this.editor.renderer.setShowGutter(false);

View File

@@ -1,21 +1,22 @@
import { Component } from "@angular/core";
// Import Ace and its modes/themes so that `ace` global is defined
import * as ace from "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/ext-settings_menu";
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-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-gruvbox";
import * as ace from "ace-builds/src-min-noconflict/ace";
import "ace-builds/src-min-noconflict/ext-settings_menu";
import "ace-builds/src-min-noconflict/ext-keybinding_menu";
import "ace-builds/src-min-noconflict/ext-command_bar";
import "ace-builds/src-min-noconflict/ext-prompt";
import "ace-builds/src-min-noconflict/ext-code_lens";
// import "ace-builds/src-min-noconflict/ext-searchbox";
import "ace-builds/src-min-noconflict/ext-language_tools";
// import "ace-builds/src-min-noconflict/theme-one_dark";
// import "ace-builds/src-min-noconflict/theme-penguins_in_space";
import "ace-builds/src-min-noconflict/theme-gruvbox";
import { CodeViewBase } from './view.base';
import { NewtonFile } from '../../common/types/file.type';
import { EditorType } from '../../common/types/editor.type';
import { ServiceMessage } from '../../common/types/service-message.type';
@@ -47,19 +48,7 @@ export class CodeViewComponent extends CodeViewBase {
private loadAce(): void {
this.configAceAndBindToElement()
if (this.isDefault) {
this.editorsService.setActiveEditor(this.uuid);
this.addActiveStyling();
}
if (this.isMiniMap) {
this.setAsMiniMapView();
return;
}
this.loadAceKeyBindings();
this.loadAceEventBindings();
this.setupRequestedMode();
}
private configAceAndBindToElement(): void {
@@ -69,10 +58,34 @@ export class CodeViewComponent extends CodeViewBase {
this.editor = ace.edit( this.editorElm.nativeElement );
this.editor.setOptions( this.editorSettings.CONFIG );
this.editorsService.set(this.uuid, this);
if (this.isDefault) {
this.editorsService.setActiveEditor(this.uuid);
this.addActiveStyling();
this.editor.focus();
}
}
private loadAceKeyBindings(): void {
private setupRequestedMode() {
switch(this.mode) {
case EditorType.Standalone:
// Note: Ace editor without any additional Newton setup...
break;
case EditorType.MiniMap:
this.setAsMiniMapView();
break;
case EditorType.ReadOnly:
this.setAsReadOnly();
break;
default:
this.loadNewtonKeyBindings();
this.loadNewtonEventBindings();
break;
}
}
private loadNewtonKeyBindings(): void {
let keyBindings = [];
for (let i = 0; i < this.editorSettings.KEYBINDINGS.length; i++) {
let keyBinding = this.editorSettings.KEYBINDINGS[i];
@@ -104,15 +117,23 @@ export class CodeViewComponent extends CodeViewBase {
this.editor.commands.addCommands( keyBindings );
}
private loadAceEventBindings(): void {
private loadNewtonEventBindings(): void {
// Note: https://ajaxorg.github.io/ace-api-docs/interfaces/ace.Ace.EditorEvents.html
this.editor.on("focus", (e) => {
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.lspManagerService.sendMessage(message);
message = new ServiceMessage();
message.action = "set-active-editor";
message.rawData = this;
this.markdownPreviewService.sendMessage(message);
this.updateInfoBar();
});
@@ -154,6 +175,12 @@ export class CodeViewComponent extends CodeViewBase {
});
this.editor.on("changeSession", (session) => {
let message = new ServiceMessage();
message.action = "set-active-editor";
message.rawData = this.editor;
this.lspManagerService.sendMessage(message);
this.updateInfoBar();
});
}

View File

@@ -4,7 +4,7 @@
<code-view [isDefault]="true"></code-view>
<hr class="col vr-pane-handle" pane-handle />
<code-view></code-view>
<code-view [isMiniMap]="true"></code-view>
<code-view [mode]="'mini-map'"></code-view>
</div>
<div>

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,11 @@
.lsp-config-text {
display: grid;
min-height: 25em;
}
.clear-left-padding {
padding-left: 0px;
}
.clear-right-padding {
padding-right: 0px;
}

View File

@@ -0,0 +1,54 @@
<div class="container-fluid">
<div class="row mt-2 mb-3">
<div class="col clear-right-padding">
<div class="input-group-sm">
<label class="form-control" [innerText]="lspManagerService.workspaceFolder || 'Project Path...'">
</label>
</div>
</div>
<div class="col col-auto clear-left-padding">
<button class="btn btn-sm btn-dark" (click)="clearWorkspaceFolder()">x</button>
</div>
<div class="col col-auto">
<div class="input-group-sm">
<button class="btn btn-sm btn-dark" (click)="setWorkspaceFolder()">Workspace Folder</button>
<button class="btn btn-sm btn-danger ms-5" (click)="hideLspManager()">X</button>
</div>
</div>
</div>
<div class="row mt-2 md-2">
<div class="col">
LSP Configs:
</div>
</div>
<div class="row">
<div class="col">
<code-view #lspEditorComponent [mode]="'standalone'" class="lsp-config-text"></code-view>
</div>
</div>
<div class="row mt-2 md-2">
<div class="col">
Target Editor: <label [innerText]="editor?.id || 'Editor...'"></label>
</div>
<div class="col">
Active Editor Session:
</div>
</div>
<div class="row">
<div class="col">
<code-view #sessionEditorComponent [mode]="'read-only'" class="lsp-config-text"></code-view>
</div>
</div>
</div>

View File

@@ -0,0 +1,117 @@
import { Component, ChangeDetectorRef, ElementRef, HostBinding, ViewChild, inject } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { LspManagerService } from '../../common/services/editor/lsp-manager/lsp-manager.service';
import { CodeViewComponent } from '../code-view/view.component';
import { ServiceMessage } from '../../common/types/service-message.type';
@Component({
selector: 'lsp-manager',
standalone: true,
imports: [
CodeViewComponent
],
templateUrl: './lsp-manager.component.html',
styleUrl: './lsp-manager.component.css',
host: {
'class': 'lsp-manager',
"(keyup)": "globalLspManagerKeyHandler($event)"
}
})
export class LspManagerComponent {
private unsubscribe: Subject<void> = new Subject();
private changeDetectorRef: ChangeDetectorRef = inject(ChangeDetectorRef);
lspManagerService: LspManagerService = inject(LspManagerService);
@HostBinding("class.hidden") isHidden: boolean = true;
@ViewChild('lspEditorComponent') lspEditorComponent!: CodeViewComponent;
@ViewChild('sessionEditorComponent') sessionEditorComponent!: CodeViewComponent;
lspTextEditor: any;
innerEditor: any;
editor: any;
constructor() {
}
private ngAfterViewInit(): void {
this.lspTextEditor = this.lspEditorComponent.editor;
this.innerEditor = this.sessionEditorComponent.editor;
this.lspManagerService.loadLspConfigData().then((lspConfigData) => {
this.lspTextEditor.session.setMode("ace/mode/json");
this.lspTextEditor.session.setValue(lspConfigData);
});
this.loadSubscribers();
}
private ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
private loadSubscribers() {
this.lspManagerService.getMessage$().pipe(
takeUntil(this.unsubscribe)
).subscribe((message: ServiceMessage) => {
if (message.action === "toggle-lsp-manager") {
this.toggleLspManager(message);
} else if (message.action === "set-active-editor") {
this.setActiveEditor(message);
}
});
}
public clearWorkspaceFolder() {
this.lspManagerService.workspaceFolder = "";
}
public setWorkspaceFolder() {
window.fs.chooseFolder().then((folder: string) => {
if (!folder) return;
this.lspManagerService.workspaceFolder = folder;
});
}
public globalLspManagerKeyHandler(event: any) {
if (event.ctrlKey && event.shiftKey && event.key === "l") {
this.hideLspManager();
}
}
public hideLspManager() {
this.isHidden = true;
this.editor.focus();
}
private toggleLspManager(message: ServiceMessage) {
this.isHidden = !this.isHidden;
if (this.isHidden) return;
// Note: hack for issue with setActiveEditor TODO
setTimeout(() => {
this.innerEditor.setSession(this.editor.session);
}, 10);
}
private setActiveEditor(message: ServiceMessage) {
this.editor = message.rawData;
// TODO: figure out why this doesn't update the session consistently...
// It seems maybe bound to visible state as change detector ref didn't help either.
// this.innerEditor.setSession(this.editor.session);
}
}

View File

@@ -0,0 +1,6 @@
<div class="row">
<div class="col">
<div [innerHtml]="bodyHtml || defaultHtml">
</div>
</div>
</div>

View File

@@ -0,0 +1,96 @@
import { Component, HostBinding, inject } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { MarkdownPreviewService } from '../../common/services/editor/markdown-preview/markdown-preview.service';
import { ServiceMessage } from '../../common/types/service-message.type';
@Component({
selector: 'markdown-preview',
standalone: true,
imports: [
],
templateUrl: './markdown-preview.component.html',
styleUrl: './markdown-preview.component.css',
host: {
'class': 'container-fluid markdown-preview'
}
})
export class MarkdownPreviewComponent {
private unsubscribe: Subject<void> = new Subject();
private markdownPreviewService: MarkdownPreviewService = inject(MarkdownPreviewService);
@HostBinding("class.hidden") isHidden: boolean = true;
converter: any = new showdown.Converter();
defaultHtml: string = "<h1>NOT a Markdown file...</h1>"
bodyHtml: string = "";
private editorComponent!: any;
constructor() {
}
private ngAfterViewInit(): void {
this.loadSubscribers();
}
private ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
private loadSubscribers() {
this.markdownPreviewService.getMessage$().pipe(
takeUntil(this.unsubscribe)
).subscribe((message: ServiceMessage) => {
if (message.action === "toggle-markdown-preview") {
this.toggleMarkdownPreview(message);
} else if (message.action === "set-active-editor") {
this.setActiveEditor(message);
}
});
}
private toggleMarkdownPreview(message: ServiceMessage) {
this.isHidden = !this.isHidden;
setTimeout(() => {
this.updatePreview();
}, 10);
}
private setActiveEditor(message: ServiceMessage) {
if (this.editorComponent == message.rawData) return;
this.editorComponent = message.rawData;
if (this.isHidden) return;
this.updatePreview();
}
public updatePreview() {
let fileMode = this.editorComponent.editor.session.getMode()["$id"];
let isMdFile = (fileMode.includes("markdown"));
this.bodyHtml = "";
if (!isMdFile) return;
let mdStr = this.editorComponent.editor.session.getValue();
let pathParts = this.editorComponent.activeFile.path.split("/");
let basePath = "file://" + pathParts.slice(0, -1).join("/");
this.bodyHtml = this.converter.makeHtml(
mdStr.replaceAll("](images", `](${basePath}/images`)
.replaceAll("](imgs", `](${basePath}/imgs`)
.replaceAll("](pictures", `](${basePath}/pictures`)
.replaceAll("](pics", `](${basePath}/pics`)
.replaceAll("](screenshots", `](${basePath}/screenshots`)
);
}
}

View File

@@ -0,0 +1,39 @@
.buttons > button {
min-width: 3.5em;
min-height: 2.5em;
}
.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 buttons">
<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.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) 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) 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

@@ -3,7 +3,7 @@
/* IDs */
#ace_settingsmenu, #kbshortcutmenu {
background-color: rgba(0, 0, 0, 0.0);
background-color: rgba(64, 64, 64, 0.84);
color: rgba(255, 255, 255, 1.0);
box-shadow: -1px 4px 5px rgba(124, 124, 124, 0.64);
padding: 1em 0.5em 2em 1em;
@@ -38,6 +38,14 @@
color: rgba(255, 255, 255, 1.0);
}
.ace_optionsMenuEntry button {
color: #000;
}
.ace_optionsMenuEntry button[ace_selected_button=true] {
background: #00e877;
}
.ace_print-margin {
width: 1px !important;
background-color: rgba(255, 0, 0, 0.84) !important;
@@ -52,4 +60,4 @@
.ace_sb-v,
.ace_sb-h {
width: 0.8em !important;
}
}

View File

@@ -0,0 +1,17 @@
/* TAGS */
html {
background-color: rgba(40, 44, 52, 0.64);
color: rgba(255, 255, 255, 0.64);
}
body {
background-color: rgba(64, 64, 64, 0.0);
overflow: hidden;
}
/* IDs */
/* CLASSES */

View File

@@ -1,25 +1,45 @@
/* TAGS */
html {
/*
background-color: rgba(64, 64, 64, 0.64);
background-color: rgb(40, 44, 52);
*/
background-color: rgba(40, 44, 52, 0.64);
color: rgba(255, 255, 255, 0.64);
}
body {
background-color: rgba(64, 64, 64, 0.0);
overflow: hidden;
}
/* IDs */
/* CLASSES */
.search-replace,
.markdown-preview,
.lsp-manager {
display: inline-block;
position: fixed;
background-color: rgba(64, 64, 64, 0.84);
overflow: auto;
}
.search-replace {
bottom: 2em;
left: 2em;
right: 2em;
z-index: 900;
}
.markdown-preview {
top: 2em;
bottom: 2em;
right: 2em;
z-index: 700;
width: 50vw;
overflow: auto;
}
.lsp-manager {
top: 2em;
bottom: 2em;
left: 2em;
right: 2em;
z-index: 800;
}
.info-bar {
font-size: 0.8em;
color: rgba(255, 255, 255, 0.84);
@@ -42,18 +62,12 @@ body {
border: 2px dashed lightblue;
}
/*
.hr-pane-handle {
.vr-pane-handle {
max-width: max-content;
height: 100%;
margin: 0px;
}
.vr-pane-handle {
transform: rotate(90deg);
}
*/
.vr-pane-handle {
max-width: 0.05em;
}
@@ -68,10 +82,6 @@ body {
}
.scroller {
/*
-webkit-scrollbar-color: #00000084 #ffffff64;
scrollbar-color: #00000084 #ffffff64;
*/
-webkit-scrollbar-color: #00000084 #ffffff06;
-webkit-scrollbar-width: thin;
scrollbar-color: #00000084 #ffffff06;

3
src/libs/showdown.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ declare global {
openFiles: (arg0) => Promise<string>,
saveFile: (arg0: any, arg1: any) => Promise<string>,
saveFileAs: () => Promise<string>,
chooseFolder: () => Promise<string>,
closeFile: (arg0: any) => Promise<string>,
getPathForFile: any,
onLoadFiles: (arg0: any) => Promise<string>,

1
src/typings.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare var showdown: any;

View File

@@ -13,4 +13,4 @@
"src/polyfills.ts",
"src/**/*.d.ts"
]
}
}

View File

@@ -16,8 +16,11 @@
"declaration": false,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true
}
"forceConsistentCasingInFileNames": true,
},
"includes": [
"src/typings.d.ts"
]
}
@@ -53,7 +56,10 @@
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
},
"includes": [
"src/typings.d.ts"
]
}
*/