UNPKG

alm

Version:

The best IDE for TypeScript

211 lines (172 loc) 6.93 kB
import utils = require("../../common/utils"); import os = require('os'); import fsu = require('../utils/fsu'); import fs = require('fs'); import chokidar = require('chokidar'); import {TypedEvent} from "../../common/events"; import {getEditorOptions} from "./editorOptions"; import {EditorOptions} from "../../common/types"; /** * Loads a file from disk * watches it on fs and then if it changes sends the new content to the client * TODO: File is *always* saved to cache for recovery * * Has a model like code mirror ... just use lines at all places ... till we actually write to disk */ export class FileModel { /** either the os default or whatever was read from the file */ private newLine: string; private text: string[] = []; /** last known state of the file system text */ private savedText: string[] = []; /** * New contents is only sent if the file has no pending changes. Otherwise it is silently ignored */ public onSavedFileChangedOnDisk = new TypedEvent<{ contents: string }>(); /** * Always emit */ public didEdits = new TypedEvent<{codeEdits: CodeEdit[];}>(); /** * Always emit */ public didStatusChange = new TypedEvent<{saved:boolean;eol:string}>(); /** * Editor config changed * Only after initial load */ public editorOptionsChanged = new TypedEvent<EditorOptions>(); /** * Editor config */ editorOptions: EditorOptions; constructor(public config: { filePath: string; }) { let contents = fsu.readFile(config.filePath); this.newLine = this.getExpectedNewline(contents); this.text = this.splitlines(contents); this.savedText = this.text.slice(); this.watchFile(); this.editorOptions = getEditorOptions(config.filePath); } getContents() { return this.text.join('\n'); } /** Returns true if the file is same as what was on disk */ edits(codeEdits: CodeEdit[]): { saved: boolean } { /** PREF: This batching can probably be made more efficient */ codeEdits.forEach(edit => { this.edit(edit); }); let saved = this.saved(); this.didEdits.emit({ codeEdits }); this.didStatusChange.emit({ saved, eol: this.newLine }); return { saved }; } private edit(codeEdit: CodeEdit) { let lastLine = this.text.length - 1; let beforeLines = this.text.slice(0, codeEdit.from.line); // there might not be any after lines. This just might be a new line :) let afterLines = codeEdit.to.line === lastLine ? [] : this.text.slice(codeEdit.to.line + 1, this.text.length); let lines = this.text.slice(codeEdit.from.line, codeEdit.to.line + 1); let content = lines.join('\n'); let contentBefore = content.substr(0, codeEdit.from.ch); let contentAfter = lines[lines.length - 1].substr(codeEdit.to.ch); content = contentBefore + codeEdit.newText + contentAfter; lines = content.split('\n'); this.text = beforeLines.concat(lines).concat(afterLines); } delete() { this.unwatchFile(); fsu.deleteFile(this.config.filePath); } _justWroteFileToDisk = false; save() { // NOTE we can never easily mutate our local `text` otherwise we have to send the changes out and sync them which is going to be nightmare const textToWrite = this.editorOptions.trimTrailingWhitespace ? this.text.map(t => t.replace(/[ \f\t\v]*$/gm, '')) : this.text; let contents = textToWrite.join(this.newLine); if (this.editorOptions.insertFinalNewline && !contents.endsWith(this.newLine)) { contents = contents + this.newLine; } fsu.writeFile(this.config.filePath, contents); this._justWroteFileToDisk = true; this.savedText = this.text.slice(); this.didStatusChange.emit({saved:true, eol: this.newLine}); } saved(): boolean { return utils.arraysEqual(this.text, this.savedText); } fileListener = (eventName: string, path: string) => { let contents = fsu.existsSync(this.config.filePath) ? fsu.readFile(this.config.filePath) : ''; let text = this.splitlines(contents); // If we wrote the file no need to do any further checks // Otherwise sometime we end up editing the file and change event fires too late and we think its new content if (this._justWroteFileToDisk) { this._justWroteFileToDisk = false; return; } // If new text same as current text nothing to do. if (arraysEqualWithWhitespace(text, this.savedText)) { return; } if (this.saved()) { this.text = text; this.savedText = this.text.slice(); this.onSavedFileChangedOnDisk.emit({ contents: this.getContents() }); } }; /** The chokidar watcher */ private fsWatcher: fs.FSWatcher = null; watchFile() { this.fsWatcher = chokidar.watch(this.config.filePath,{ignoreInitial: true}); this.fsWatcher.on('change',this.fileListener); } unwatchFile() { this.fsWatcher.close(); this.fsWatcher = null; } /** Just updates `text` saves */ setContents(contents: string) { this.text = this.splitlines(contents); this.save(); } /** * Someone else should call this if an editor config file changes * Here we need to re-evalute our options */ recheckEditorOptions() { this.editorOptions = getEditorOptions(this.config.filePath); this.editorOptionsChanged.emit(this.editorOptions); } /** Great for error messages etc. Ofcourse `0` based */ getLinePreview(line: number) { return this.text[line]; } /** * split lines * https://github.com/codemirror/CodeMirror/blob/5738f9b2cff5241ea13e32db3579eb347e56e7a0/lib/codemirror.js#L8594 */ private splitlines(string: string) { return string.split(/\r\n?|\n/); }; /** https://github.com/sindresorhus/detect-newline/blob/master/index.js */ private getExpectedNewline(str: string) { var newlines = (str.match(/(?:\r?\n)/g) || []); var crlf = newlines.filter(function (el) { return el === '\r\n'; }).length; var lf = newlines.length - crlf; // My addition if (lf == 0 && crlf == 0) return os.EOL; return crlf > lf ? '\r\n' : '\n'; } } /** * shallow equality of sorted string arrays that considers whitespace to be insignificant */ function arraysEqualWithWhitespace(a: string[], b: string[]): boolean { if (a === b) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; for (var i = 0; i < a.length; ++i) { if (a[i].trim() !== b[i].trim()) return false; } return true; }