2021-04-18 18:49:53 +02:00
|
|
|
namespace OS {
|
2021-11-22 18:51:29 +01:00
|
|
|
//declare var $:any;
|
2021-04-18 18:49:53 +02:00
|
|
|
export namespace application {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extends the [[RemoteFileHandle]] interface with some useful
|
|
|
|
* properties used by [[BaseEditorModel]]
|
|
|
|
*/
|
|
|
|
export type EditorFileHandle = API.VFS.RemoteFileHandle & {
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2021-11-22 18:51:29 +01:00
|
|
|
/**
|
|
|
|
* Editor status monitoring
|
|
|
|
*/
|
2021-04-18 18:49:53 +02:00
|
|
|
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
|
|
|
|
this.tabbar.push(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close a tab when a file is closed
|
|
|
|
*
|
|
|
|
* @param {GUI.tag.ListViewItemTag} it reference to the tab to close
|
|
|
|
* @returns {boolean}
|
|
|
|
* @memberof BaseEditorModel
|
|
|
|
*/
|
2021-11-22 18:51:29 +01:00
|
|
|
closeTab(it: GUI.tag.ListViewItemTag): boolean {
|
2021-04-18 18:49:53 +02:00
|
|
|
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);
|
|
|
|
if (this.onstatuschange)
|
|
|
|
this.onstatuschange(this.getEditorStatus());
|
|
|
|
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(file: EditorFileHandle): void {
|
|
|
|
this.currfile.cache = this.getValue();
|
|
|
|
file.write("text/plain")
|
|
|
|
.then((d) => {
|
|
|
|
file.dirty = false;
|
|
|
|
file.text = file.basename;
|
|
|
|
this.tabbar.update(undefined);
|
|
|
|
})
|
|
|
|
.catch((e) =>
|
|
|
|
this.app.error(__("Unable to save file: {0}", file.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(this.currfile);
|
|
|
|
}
|
|
|
|
return this.saveAs();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(this.currfile);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.resetEditor();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether the editor is dirty
|
|
|
|
*
|
|
|
|
* @return {*} {boolean}
|
|
|
|
* @memberof BaseEditorModel
|
|
|
|
*/
|
|
|
|
isDirty(): boolean {
|
|
|
|
return this.dirties().length > 0;
|
|
|
|
}
|
|
|
|
|
2021-11-22 18:51:29 +01:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
2021-04-18 18:49:53 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|