antosdk-apps/Antedit/ts/BaseEditorModel.ts

710 lines
22 KiB
TypeScript

namespace OS {
export namespace API
{
export namespace VFS
{
export class DiffEditorFileHandle extends OS.API.VFS.BaseFileHandle
{
text: string;
textModel:any;
selected: boolean;
constructor(files: OS.API.VFS.BaseFileHandle[]) {
super("");
this.path = `${files[0].path} -> ${files[1].path}`;
this.cache = files;
this.basename = `${files[0].basename} -> ${files[1].basename}`;
this.info = {
type: "file",
mime: undefined,
size: 0,
name: this.basename,
path: this.path
};
this.ready = true;
}
meta(): Promise<any> {
return new Promise(async (resolve, reject) =>
{
try
{
await Promise.all([this.cache[0].meta(), this.cache[1].meta]);
resolve({
result: this.info,
error: false,
});
}
catch(e)
{
reject(e);
}
});
}
protected _rd(_t: string): Promise<any> {
return new Promise(async (resolve, reject) => {
try
{
this.cache[0].cache = await this.cache[0].read();
this.cache[1].cache = await this.cache[1].read();
resolve(this.cache);
}
catch(e)
{
reject(e);
}
});
}
protected _wr(t: string): Promise<any> {
return new Promise((resolve, reject) =>
{
resolve({
result: true,
error: false,
})
});
}
setPath(s: string)
{
// do nothing
}
}
}
}
export namespace application {
/**
* Extends the [[RemoteFileHandle]] interface with some useful
* properties used by [[BaseEditorModel]]
*/
export type EditorFileHandle = OS.API.VFS.BaseFileHandle & {
/**
* The text will be displayed on the tab bar when opened
*
* @type {string}
*/
text: string;
/**
* Editor text model attached to the file
* modification history of the file
*
* @type {any}
*/
textModel: any;
/**
* Indicate whether the file is selected
*
* @type {boolean}
*/
selected: boolean;
};
export abstract class BaseEditorModel {
/**
* Reference to the current editing file handle
*
* @protected
* @type {EditorFileHandle}
* @memberof BaseEditorModel
*/
protected currfile: EditorFileHandle;
/**
* Referent to the parent app
*
* @private
* @type {BaseApplication}
* @memberof BaseEditorModel
*/
private app: BaseApplication;
/**
* Reference to the editor tab bar UI
*
* @private
* @type {GUI.tag.TabBarTag}
* @memberof BaseEditorModel
*/
private tabbar: GUI.tag.TabBarTag;
/**
* Referent to the editor container
*
* @private
* @type {HTMLElement}
* @memberof BaseEditorModel
*/
private container: HTMLElement;
/**
* Editor status monitoring
*/
onstatuschange: (stat: GenericObject<any>) => void;
/**
* Editor mutex
*
* @private
* @type {boolean}
* @memberof BaseEditorModel
*/
private editormux: boolean;
/**
* Creates an instance of BaseEditorModel.
*
* @param {Antedit} app parent app
* @param {GUI.tag.TabBarTag} tabbar tabbar DOM element
* @param {HTMLElement} editorarea editor container DOM element
* @memberof BaseEditorModel
*/
constructor(app: BaseApplication, tabbar: GUI.tag.TabBarTag, editorarea: HTMLElement) {
this.container = editorarea;
this.currfile = "Untitled".asFileHandle() as EditorFileHandle;
this.tabbar = tabbar;
this.editorSetup(editorarea);
this.app = app;
this.editormux = false;
this.onstatuschange = undefined;
this.on("focus", () => {
if (this.onstatuschange)
this.onstatuschange(this.getEditorStatus());
});
this.on("input", () => {
if (this.editormux) {
this.editormux = false;
return false;
}
if (!this.currfile.dirty) {
this.currfile.dirty = true;
this.currfile.text += "*";
return this.tabbar.update(undefined);
}
});
this.on("changeCursor", () => {
if (this.onstatuschange)
this.onstatuschange(this.getEditorStatus());
});
this.tabbar.ontabselect = (e) => {
return this.selecteTab($(e.data.item).index());
};
this.tabbar.ontabclose = (e) => {
const it = e.data.item;
if (!it) {
return false;
}
if (!it.data.dirty) {
return this.closeTab(it);
}
this.app.openDialog("YesNoDialog", {
title: __("Close tab"),
text: __("Close without saving ?"),
}).then((d) => {
if (d) {
return this.closeTab(it);
}
return this.focus();
});
return false;
};
}
/**
* Find a tab on the tabbar corresponding to a file handle
*
* @private
* @param {EditorFileHandle} file then file handle to search
* @returns {number}
* @memberof BaseEditorModel
*/
private findTabByFile(file: EditorFileHandle): number {
const lst = this.tabbar.items;
const its = (() => {
const result = [];
for (let i = 0; i < lst.length; i++) {
const d = lst[i];
if (d.hash() === file.hash()) {
result.push(i);
}
}
return result;
})();
if (its.length === 0) {
return -1;
}
return its[0];
}
/**
* Create new tab when opening a file
*
* @private
* @param {EditorFileHandle} file
* @memberof BaseEditorModel
*/
private newTab(file: EditorFileHandle): void {
file.text = file.basename ? file.basename : file.path;
if (!file.cache) {
file.cache = "";
}
file.textModel = this.newTextModelFrom(file);
this.currfile.selected = false;
file.selected = true;
//console.log cnt
const el = this.tabbar.push(file);
this.app.trigger("tab-opened", el);
}
/**
* Close a tab when a file is closed
*
* @param {GUI.tag.ListViewItemTag} it reference to the tab to close
* @returns {boolean}
* @memberof BaseEditorModel
*/
closeTab(it: GUI.tag.ListViewItemTag): boolean {
this.tabbar.delete(it);
const cnt = this.tabbar.items.length;
if (cnt === 0) {
this.openFile(
"Untitled".asFileHandle() as EditorFileHandle
);
return false;
}
this.tabbar.selected = cnt - 1;
return false;
}
/**
* Select a tab by its index
*
* @private
* @param {number} i tab index
* @returns {void}
* @memberof BaseEditorModel
*/
private selecteTab(i: number): void {
//return if i is @tabbar.get "selidx"
const file = this.tabbar.items[i] as EditorFileHandle;
if (!file) {
return;
}
//return if file is @currfile
if (this.currfile !== file) {
this.currfile.textModel = this.getTexModel();
this.currfile.selected = false;
this.currfile = file;
}
// this.editormux = true;
this.setTextModel(file.textModel);
this.focus();
}
/**
* Select an opened file, this will select the corresponding tab
*
* @param {(EditorFileHandle | string)} file
* @memberof BaseEditorModel
*/
selectFile(file: EditorFileHandle | string): void {
const i = this.findTabByFile(
file.asFileHandle() as EditorFileHandle
);
if (i !== -1) {
this.tabbar.selected = i;
}
}
/**
* Open a file in new tab. If the file is already opened,
* the just select the tab
*
*
* @param {EditorFileHandle} file file to open
* @returns {void}
* @memberof BaseEditorModel
*/
openFile(file: EditorFileHandle): void {
//find tab
const i = this.findTabByFile(file);
if (i !== -1) {
this.tabbar.selected = i;
return;
}
if (file.path.toString() === "Untitled") {
this.newTab(file);
return;
}
file.read()
.then((d) => {
file.cache = d || "";
return this.newTab(file);
})
.catch((e) => {
return this.app.error(
__("Unable to open: {0}", file.path),
e
);
});
}
/**
* write a file
*
* @private
* @param {EditorFileHandle} file
* @memberof BaseEditorModel
*/
private write(): void {
this.currfile.cache = this.getValue();
this.currfile.write("text/plain")
.then((d) => {
this.currfile.dirty = false;
this.currfile.text = this.currfile.basename;
this.tabbar.update(undefined);
})
.catch((e) =>
this.app.error(__("Unable to save file: {0}", this.currfile.path), e)
);
}
/**
* Save the current opened file
*
* @return {*} {void}
* @memberof BaseEditorModel
*/
save(): void {
this.currfile.cache = this.getValue();
if (this.currfile.basename) {
return this.write();
}
return this.saveAs();
}
/**
* Reload the current openned file
*
* @return {*} {Promise<any>}
* @memberof BaseEditorModel
*/
reload(): Promise<any> {
return new Promise(async (ok,r) =>{
try {
if (this.currfile.path.toString() === "Untitled") {
return ok(true);
}
if(this.currfile.dirty)
{
const ret = await this.app.openDialog("YesNoDialog", {
title: __("File modified"),
text: __("Continue without saving ?"),
});
if(!ret)
{
return ok(true);
}
}
const d = await this.currfile.read();
this.currfile.cache = d || "";
this.currfile.dirty = false;
this.currfile.text = this.currfile.basename ? this.currfile.basename : this.currfile.path;
this.editormux = true;
this.setValue(this.currfile.cache);
this.tabbar.update(undefined);
}
catch(e){
this.app.error(
__("Unable to open: {0}", this.currfile.path),
e
);
r(e);
};
})
}
/**
* Save the current file as another file
*
* @public
* @memberof BaseEditorModel
*/
saveAs(): void {
this.app.openDialog("FileDialog", {
title: __("Save as"),
file: this.currfile,
}).then((f) => {
let d = f.file.path.asFileHandle();
if (f.file.type === "file") {
d = d.parent();
}
this.currfile.setPath(`${d.path}/${f.name}`);
this.write();
});
}
/**
* Get all dirty file handles in the editor
*
* @return {*} {EditorFileHandle[]}
* @memberof BaseEditorModel
*/
dirties(): EditorFileHandle[] {
const result = [];
for (let v of Array.from(this.tabbar.items)) {
if (v.dirty) {
result.push(v);
}
}
return result;
}
/**
* Context menu handle for the editor
*
* @memberof BaseEditorModel
*/
set contextmenuHandle(cb: (e: any, m: any) => void) {
this.container.contextmenuHandle = cb;
}
/**
* Close all opened files
*
* @memberof BaseEditorModel
*/
closeAll(): void {
this.tabbar.items = [];
this.openFile("Untitled".asFileHandle() as EditorFileHandle);
this.resetEditor();
}
/**
* Check whether the editor is dirty
*
* @return {*} {boolean}
* @memberof BaseEditorModel
*/
isDirty(): boolean {
return this.dirties().length > 0;
}
/**
* Set model tabbar menu context event
*
* @memberof BaseEditorModel
*/
setTabbarCtxMenu(items: GenericObject<any>[], handle:(tab:GUI.tag.ListViewItemTag, mdata: GenericObject<any>) => void): void
{
this.tabbar.contextmenuHandle = (evt, m) => {
m.items = items;
m.onmenuselect = (e) => {
if(handle)
{
/**
* get the tab under the cursor
*/
const tab = $(evt.target).closest("afx-list-item");
handle(tab[0], e.data.item.data);
}
};
return m.show(evt);
}
}
/**
* Set up the editor instance
* Should be implemented by subclass
*
* @protected
* @abstract
* @param {HTMLElement} el
* @memberof BaseEditorModel
*/
protected abstract editorSetup(el: HTMLElement): void;
/**
* Listen to editor event
*
* Should be implemented by subclasses
*
* @abstract
* @param {string} evt_str
* @param {() => void} callback
* @memberof BaseEditorModel
*/
abstract on(evt_str: string, callback: () => void): void;
/**
* Resize the editor
*
* Should be implemented by subclasses
*
* @abstract
* @memberof BaseEditorModel
*/
abstract resize(): void;
/**
* Make the editor focused
*
* Should be implemented by subclasses
*
* @abstract
* @memberof BaseEditorModel
*/
abstract focus(): void;
/**
* Get language mode from file extension
*
* Should be implemented by subclasses
*
* @protected
* @abstract
* @param {string} path
* @return {*} {GenericObject<any>}
* @memberof BaseEditorModel
*/
protected abstract getModeForPath(path: string): GenericObject<any>;
/**
* Query the editor status
*
* Should be implemented by subclasses
*
* @abstract
* @return {*} {GenericObject<any>}
* @memberof BaseEditorModel
*/
abstract getEditorStatus(): GenericObject<any>;
/**
* Get the editor value
*
* Should be implemented by subclasses
*
* @abstract
* @return {*} {string}
* @memberof BaseEditorModel
*/
abstract getValue(): string;
/**
* Set the editor value
*
* Should be implemented by subclasses
*
* @abstract
* @param {string} value
* @memberof BaseEditorModel
*/
abstract setValue(value: string): void;
/**
* Set the editor language mode
*
* Should be implemented by subclasses
*
* @abstract
* @param {GenericObject<any>} m
* @memberof BaseEditorModel
*/
abstract setMode(m: GenericObject<any>): void;
/**
* Get textModel from the current editor session
*
* @protected
* @abstract
* @return {*} {*}
* @memberof BaseEditorModel
*/
protected abstract getTexModel(): any;
/**
* Set text model to the current editor session
*
* @protected
* @abstract
* @param {*} model
* @memberof BaseEditorModel
*/
protected abstract setTextModel(model: any): void;
/**
* Create new text model from the VFS file
*
* @protected
* @abstract
* @param {EditorFileHandle} file
* @return {*} {*}
* @memberof BaseEditorModel
*/
protected abstract newTextModelFrom(file: EditorFileHandle): any;
/**
* Reset the editor
*
* @protected
* @abstract
* @memberof BaseEditorModel
*/
protected abstract resetEditor(): void;
/**
* Set the current editor theme
*
* Should be implemented by subclasses
*
* @abstract
* @param {string} theme
* @memberof BaseEditorModel
*/
abstract setTheme(theme: string): void;
/**
* Get all language modes supported by the editor
*
* @abstract
* @return {*} {GenericObject<any>[]}
* @memberof BaseEditorModel
*/
abstract getModes(): GenericObject<any>[];
/**
* Get the real editor model
*/
abstract getEditor(): any;
}
}
}