/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.9-prerelease
src/document/Document.js
767 строк
34 KB
ficristo
Move most inline jslint directives to config files
12 авг 2016, 10:22
12 авг 2016, 10:22
074382a
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ define(function (require, exports, module) { "use strict"; var EditorManager = require("editor/EditorManager"), EventDispatcher = require("utils/EventDispatcher"), FileUtils = require("file/FileUtils"), InMemoryFile = require("document/InMemoryFile"), PerfUtils = require("utils/PerfUtils"), LanguageManager = require("language/LanguageManager"), CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"), _ = require("thirdparty/lodash"); /** * Model for the contents of a single file and its current modification state. * See DocumentManager documentation for important usage notes. * * Document dispatches these events: * * __change__ -- When the text of the editor changes (including due to undo/redo). * * Passes ({Document}, {ChangeList}), where ChangeList is an array * of change record objects. Each change record looks like: * * { from: start of change, expressed as {line: <line number>, ch: <character offset>}, * to: end of change, expressed as {line: <line number>, ch: <chracter offset>}, * text: array of lines of text to replace existing text } * * The line and ch offsets are both 0-based. * * The ch offset in "from" is inclusive, but the ch offset in "to" is exclusive. For example, * an insertion of new content (without replacing existing content) is expressed by a range * where from and to are the same. * * If "from" and "to" are undefined, then this is a replacement of the entire text content. * * IMPORTANT: If you listen for the "change" event, you MUST also addRef() the document * (and releaseRef() it whenever you stop listening). You should also listen to the "deleted" * event. * * __deleted__ -- When the file for this document has been deleted. All views onto the document should * be closed. The document will no longer be editable or dispatch "change" events. * * __languageChanged__ -- When the value of getLanguage() has changed. 2nd argument is the old value, * 3rd argument is the new value. * * @constructor * @param {!File} file Need not lie within the project. * @param {!Date} initialTimestamp File's timestamp when we read it off disk. * @param {!string} rawText Text content of the file. */ function Document(file, initialTimestamp, rawText) { this.file = file; this._updateLanguage(); this.refreshText(rawText, initialTimestamp, true); // List of full editors which are initialized as master editors for this doc. this._associatedFullEditors = []; } EventDispatcher.makeEventDispatcher(Document.prototype); /** * Number of clients who want this Document to stay alive. The Document is listed in * DocumentManager._openDocuments whenever refCount > 0. */ Document.prototype._refCount = 0; /** * The File for this document. Need not lie within the project. * If Document is untitled, this is an InMemoryFile object. * @type {!File} */ Document.prototype.file = null; /** * The Language for this document. Will be resolved by file extension in the constructor * @type {!Language} */ Document.prototype.language = null; /** * Whether this document has unsaved changes or not. * When this changes on any Document, DocumentManager dispatches a "dirtyFlagChange" event. * @type {boolean} */ Document.prototype.isDirty = false; /** * Whether this document is currently being saved. * @type {boolean} */ Document.prototype.isSaving = false; /** * What we expect the file's timestamp to be on disk. If the timestamp differs from this, then * it means the file was modified by an app other than Brackets. * @type {!Date} */ Document.prototype.diskTimestamp = null; /** * The timestamp of the document at the point where the user last said to keep changes that conflict * with the current disk version. Can also be -1, indicating that the file was deleted on disk at the * last point when the user said to keep changes, or null, indicating that the user has not said to * keep changes. * Note that this is a time as returned by Date.getTime(), not a Date object. * @type {?Number} */ Document.prototype.keepChangesTime = null; /** * True while refreshText() is in progress and change notifications shouldn't trip the dirty flag. * @type {boolean} */ Document.prototype._refreshInProgress = false; /** * The text contents of the file, or null if our backing model is _masterEditor. * @type {?string} */ Document.prototype._text = null; /** * Editor object representing the full-size editor UI for this document. May be null if Document * has not yet been modified or been the currentDocument; in that case, our backing model is the * string _text. * @type {?Editor} */ Document.prototype._masterEditor = null; /** * The content's line-endings style. If a Document is created on empty text, or text with * inconsistent line endings, defaults to the current platform's standard endings. * @type {FileUtils.LINE_ENDINGS_CRLF|FileUtils.LINE_ENDINGS_LF} */ Document.prototype._lineEndings = null; /** Add a ref to keep this Document alive */ Document.prototype.addRef = function () { //console.log("+++REF+++ "+this); if (this._refCount === 0) { //console.log("+++ adding to open list"); if (exports.trigger("_afterDocumentCreate", this)) { return; } } this._refCount++; }; /** Remove a ref that was keeping this Document alive */ Document.prototype.releaseRef = function () { //console.log("---REF--- "+this); this._refCount--; if (this._refCount < 0) { console.error("Document ref count has fallen below zero!"); return; } if (this._refCount === 0) { //console.log("--- removing from open list"); if (exports.trigger("_beforeDocumentDelete", this)) { return; } } }; /** * Attach a backing Editor to the Document, enabling setText() to be called. Assumes Editor has * already been initialized with the value of getText(). ONLY Editor should call this (and only * when EditorManager has told it to act as the master editor). * @param {!Editor} masterEditor */ Document.prototype._makeEditable = function (masterEditor) { this._text = null; this._masterEditor = masterEditor; masterEditor.on("change", this._handleEditorChange.bind(this)); }; /** * Detach the backing Editor from the Document, disallowing setText(). The text content is * stored back onto _text so other Document clients continue to have read-only access. ONLY * Editor.destroy() should call this. */ Document.prototype._makeNonEditable = function () { if (!this._masterEditor) { console.error("Document is already non-editable"); } else { // _text represents the raw text, so fetch without normalized line endings this._text = this.getText(true); this._associatedFullEditors.splice(this._associatedFullEditors.indexOf(this._masterEditor), 1); // Identify the most recently created full editor before this and set that as new master editor if (this._associatedFullEditors.length > 0) { this._masterEditor = this._associatedFullEditors[this._associatedFullEditors.length - 1]; } else { this._masterEditor = null; } } }; /** * Toggles the master editor which has gained focus from a pool of full editors * To be used internally by Editor only */ Document.prototype._toggleMasterEditor = function (masterEditor) { // Do a check before processing the request to ensure inline editors are not being set as master editor if (this.file === masterEditor.document.file && this._associatedFullEditors.indexOf(masterEditor) >= 0) { this._masterEditor = masterEditor; } }; /** * Checks and returns if a full editor exists for the provided pane attached to this document * @param {String} paneId * @return {Editor} Attached editor bound to the provided pane id */ Document.prototype._checkAssociatedEditorForPane = function (paneId) { var editorCount, editorForPane; for (editorCount = 0; editorCount < this._associatedFullEditors.length; ++editorCount) { if (this._associatedFullEditors[editorCount]._paneId === paneId) { editorForPane = this._associatedFullEditors[editorCount]; break; } } return editorForPane; }; /** * Disassociates an editor from this document if present in the associated editor list * To be used internally by Editor only when destroyed and not the current master editor for the document */ Document.prototype._disassociateEditor = function (editor) { // Do a check before processing the request to ensure inline editors are not being handled if (this._associatedFullEditors.indexOf(editor) >= 0) { this._associatedFullEditors.splice(this._associatedFullEditors.indexOf(editor), 1); } }; /** * Aassociates a full editor to this document * To be used internally by Editor only when pane marking happens */ Document.prototype._associateEditor = function (editor) { // Do a check before processing the request to ensure inline editors are not being handled if (this._associatedFullEditors.indexOf(editor) === -1) { this._associatedFullEditors.push(editor); } }; /** * Guarantees that _masterEditor is non-null. If needed, asks EditorManager to create a new master * editor bound to this Document (which in turn causes Document._makeEditable() to be called). * Should ONLY be called by Editor and Document. */ Document.prototype._ensureMasterEditor = function () { if (!this._masterEditor) { EditorManager._createUnattachedMasterEditor(this); } }; /** * Returns the document's current contents; may not be saved to disk yet. Whenever this * value changes, the Document dispatches a "change" event. * * @param {boolean=} useOriginalLineEndings If true, line endings in the result depend on the * Document's line endings setting (based on OS & the original text loaded from disk). * If false, line endings are always \n (like all the other Document text getter methods). * @return {string} */ Document.prototype.getText = function (useOriginalLineEndings) { if (this._masterEditor) { // CodeMirror.getValue() always returns text with LF line endings; fix up to match line // endings preferred by the document, if necessary var codeMirrorText = this._masterEditor._codeMirror.getValue(); if (useOriginalLineEndings) { if (this._lineEndings === FileUtils.LINE_ENDINGS_CRLF) { return codeMirrorText.replace(/\n/g, "\r\n"); } } return codeMirrorText; } else { // Optimized path that doesn't require creating master editor if (useOriginalLineEndings) { return this._text; } else { return Document.normalizeText(this._text); } } }; /** Normalizes line endings the same way CodeMirror would */ Document.normalizeText = function (text) { return text.replace(/\r\n/g, "\n"); }; /** * Sets the contents of the document. Treated as an edit. Line endings will be rewritten to * match the document's current line-ending style. * @param {!string} text The text to replace the contents of the document with. */ Document.prototype.setText = function (text) { this._ensureMasterEditor(); this._masterEditor._codeMirror.setValue(text); // _handleEditorChange() triggers "change" event }; /** * @private * Triggers the appropriate events when a change occurs: "change" on the Document instance * and "documentChange" on the Document module. * @param {Object} changeList Changelist in CodeMirror format */ Document.prototype._notifyDocumentChange = function (changeList) { this.trigger("change", this, changeList); exports.trigger("documentChange", this, changeList); }; /** * Sets the contents of the document. Treated as reloading the document from disk: the document * will be marked clean with a new timestamp, the undo/redo history is cleared, and we re-check * the text's line-ending style. CAN be called even if there is no backing editor. * @param {!string} text The text to replace the contents of the document with. * @param {!Date} newTimestamp Timestamp of file at the time we read its new contents from disk. * @param {boolean} initial True if this is the initial load of the document. In that case, * we don't send change events. */ Document.prototype.refreshText = function (text, newTimestamp, initial) { var perfTimerName = PerfUtils.markStart("refreshText:\t" + (!this.file || this.file.fullPath)); // If clean, don't transiently mark dirty during refresh // (we'll still send change events though, of course) this._refreshInProgress = true; if (this._masterEditor) { this._masterEditor._resetText(text); // clears undo history too // _handleEditorChange() triggers "change" event for us } else { this._text = text; if (!initial) { // We fake a change record here that looks like CodeMirror's text change records, but // omits "from" and "to", by which we mean the entire text has changed. // TODO: Dumb to split it here just to join it again in the change handler, but this is // the CodeMirror change format. Should we document our change format to allow this to // either be an array of lines or a single string? this._notifyDocumentChange([{text: text.split(/\r?\n/)}]); } } this._updateTimestamp(newTimestamp); // If Doc was dirty before refresh, reset it to clean now (don't always call, to avoid no-op dirtyFlagChange events) Since // _resetText() above already ensures Editor state is clean, it's safe to skip _markClean() as long as our own state is already clean too. if (this.isDirty) { this._markClean(); } this._refreshInProgress = false; // Sniff line-ending style this._lineEndings = FileUtils.sniffLineEndings(text); if (!this._lineEndings) { this._lineEndings = FileUtils.getPlatformLineEndings(); } exports.trigger("_documentRefreshed", this); PerfUtils.addMeasurement(perfTimerName); }; /** * Adds, replaces, or removes text. If a range is given, the text at that range is replaced with the * given new text; if text == "", then the entire range is effectively deleted. If 'end' is omitted, * then the new text is inserted at that point and all existing text is preserved. Line endings will * be rewritten to match the document's current line-ending style. * * IMPORTANT NOTE: Because of #1688, do not use this in cases where you might be * operating on a linked document (like the main document for an inline editor) * during an outer CodeMirror operation (like a key event that's handled by the * editor itself). A common case of this is code hints in inline editors. In * such cases, use `editor._codeMirror.replaceRange()` instead. This should be * fixed when we migrate to use CodeMirror's native document-linking functionality. * * @param {!string} text Text to insert or replace the range with * @param {!{line:number, ch:number}} start Start of range, inclusive (if 'to' specified) or insertion point (if not) * @param {?{line:number, ch:number}} end End of range, exclusive; optional * @param {?string} origin Optional string used to batch consecutive edits for undo. * If origin starts with "+", then consecutive edits with the same origin will be batched for undo if * they are close enough together in time. * If origin starts with "*", then all consecutive edit with the same origin will be batched for * undo. * Edits with origins starting with other characters will not be batched. * (Note that this is a higher level of batching than batchOperation(), which already batches all * edits within it for undo. Origin batching works across operations.) */ Document.prototype.replaceRange = function (text, start, end, origin) { this._ensureMasterEditor(); this._masterEditor._codeMirror.replaceRange(text, start, end, origin); // _handleEditorChange() triggers "change" event }; /** * Returns the characters in the given range. Line endings are normalized to '\n'. * @param {!{line:number, ch:number}} start Start of range, inclusive * @param {!{line:number, ch:number}} end End of range, exclusive * @return {!string} */ Document.prototype.getRange = function (start, end) { this._ensureMasterEditor(); return this._masterEditor._codeMirror.getRange(start, end); }; /** * Returns the text of the given line (excluding any line ending characters) * @param {number} Zero-based line number * @return {!string} */ Document.prototype.getLine = function (lineNum) { this._ensureMasterEditor(); return this._masterEditor._codeMirror.getLine(lineNum); }; /** * Batches a series of related Document changes. Repeated calls to replaceRange() should be wrapped in a * batch for efficiency. Begins the batch, calls doOperation(), ends the batch, and then returns. * @param {function()} doOperation */ Document.prototype.batchOperation = function (doOperation) { this._ensureMasterEditor(); var self = this; self._masterEditor._codeMirror.operation(doOperation); }; /** * Handles changes from the master backing Editor. Changes are triggered either by direct edits * to that Editor's UI, OR by our setText()/refreshText() methods. * @private */ Document.prototype._handleEditorChange = function (event, editor, changeList) { // Handle editor change event only when it is originated from the master editor for this doc if (this._masterEditor !== editor) { return; } // TODO: This needs to be kept in sync with SpecRunnerUtils.createMockActiveDocument(). In the // future, we should fix things so that we either don't need mock documents or that this // is factored so it will just run in both. if (!this._refreshInProgress) { // Sync isDirty from CodeMirror state var wasDirty = this.isDirty; this.isDirty = !editor._codeMirror.isClean(); // Notify if isDirty just changed (this also auto-adds us to working set if needed) if (wasDirty !== this.isDirty) { exports.trigger("_dirtyFlagChange", this); } } // Notify that Document's text has changed this._notifyDocumentChange(changeList); }; /** * @private */ Document.prototype._markClean = function () { this.isDirty = false; if (this._masterEditor) { this._masterEditor._codeMirror.markClean(); } exports.trigger("_dirtyFlagChange", this); }; /** * @private */ Document.prototype._updateTimestamp = function (timestamp) { this.diskTimestamp = timestamp; // Clear the "keep changes" timestamp since it's no longer relevant. this.keepChangesTime = null; }; /** * Called when the document is saved (which currently happens in DocumentCommandHandlers). Marks the * document not dirty and notifies listeners of the save. */ Document.prototype.notifySaved = function () { if (!this._masterEditor) { console.log("### Warning: saving a Document that is not modifiable!"); } this._markClean(); // TODO: (issue #295) fetching timestamp async creates race conditions (albeit unlikely ones) var thisDoc = this; this.file.stat(function (err, stat) { if (!err) { thisDoc._updateTimestamp(stat.mtime); } else { console.log("Error updating timestamp after saving file: " + thisDoc.file.fullPath); } exports.trigger("_documentSaved", thisDoc); }); }; /** * Adjusts a given position taking a given replaceRange-type edit into account. * If the position is within the original edit range (start and end inclusive), * it gets pushed to the end of the content that replaced the range. Otherwise, * if it's after the edit, it gets adjusted so it refers to the same character * it did before the edit. * @param {!{line:number, ch: number}} pos The position to adjust. * @param {!Array.<string>} textLines The text of the change, split into an array of lines. * @param {!{line: number, ch: number}} start The start of the edit. * @param {!{line: number, ch: number}} end The end of the edit. * @return {{line: number, ch: number}} The adjusted position. */ Document.prototype.adjustPosForChange = function (pos, textLines, start, end) { // Same as CodeMirror.adjustForChange(), but that's a private function // and Marijn would rather not expose it publicly. var change = { text: textLines, from: start, to: end }; if (CodeMirror.cmpPos(pos, start) < 0) { return pos; } if (CodeMirror.cmpPos(pos, end) <= 0) { return CodeMirror.changeEnd(change); } var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; if (pos.line === change.to.line) { ch += CodeMirror.changeEnd(change).ch - change.to.ch; } return {line: line, ch: ch}; }; /** * Like _.each(), but if given a single item not in an array, acts as * if it were an array containing just that item. */ function oneOrEach(itemOrArr, cb) { if (Array.isArray(itemOrArr)) { _.each(itemOrArr, cb); } else { cb(itemOrArr, 0); } } /** * Helper function for edit operations that operate on multiple selections. Takes an "edit list" * that specifies a list of replaceRanges that should occur, but where all the positions are with * respect to the document state before all the edits (i.e., you don't have to figure out how to fix * up the selections after each sub-edit). Edits must be non-overlapping (in original-document terms). * All the edits are done in a single batch. * * If your edits are structured in such a way that each individual edit would cause its associated * selection to be properly updated, then all you need to specify are the edits themselves, and the * selections will automatically be updated as the edits are performed. However, for some * kinds of edits, you need to fix up the selection afterwards. In that case, you can specify one * or more selections to be associated with each edit. Those selections are assumed to be in terms * of the document state after the edit, *as if* that edit were the only one being performed (i.e., * you don't have to worry about adjusting for the effect of other edits). If you supply these selections, * then this function will adjust them as necessary for the effects of other edits, and then return a * flat list of all the selections, suitable for passing to `setSelections()`. * * @param {!Array.<{edit: {text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}} * | Array.<{text: string, start:{line: number, ch: number}, end:?{line: number, ch: number}}>, * selection: ?{start:{line:number, ch:number}, end:{line:number, ch:number}, * primary:boolean, reversed: boolean, isBeforeEdit: boolean}>} * | ?Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, * primary:boolean, reversed: boolean, isBeforeEdit: boolean}>}>} edits * Specifies the list of edits to perform in a manner similar to CodeMirror's `replaceRange`. This array * will be mutated. * * `edit` is the edit to perform: * `text` will replace the current contents of the range between `start` and `end`. * If `end` is unspecified, the text is inserted at `start`. * `start` and `end` should be positions relative to the document *ignoring* all other edit descriptions * (i.e., as if you were only performing this one edit on the document). * If any of the edits overlap, an error will be thrown. * * If `selection` is specified, it should be a selection associated with this edit. * If `isBeforeEdit` is set on the selection, the selection will be fixed up for this edit. * If not, it won't be fixed up for this edit, meaning it should be expressed in terms of * the document state after this individual edit is performed (ignoring any other edits). * Note that if you were planning on just specifying `isBeforeEdit` for every selection, you can * accomplish the same thing by simply not passing any selections and letting the editor update * the existing selections automatically. * * Note that `edit` and `selection` can each be either an individual edit/selection, or a group of * edits/selections to apply in order. This can be useful if you need to perform multiple edits in a row * and then specify a resulting selection that shouldn't be fixed up for any of those edits (but should be * fixed up for edits related to other selections). It can also be useful if you have several selections * that should ignore the effects of a given edit because you've fixed them up already (this commonly happens * with line-oriented edits where multiple cursors on the same line should be ignored, but still tracked). * Within an edit group, edit positions must be specified relative to previous edits within that group. Also, * the total bounds of edit groups must not overlap (e.g. edits in one group can't surround an edit from another group). * * @param {?string} origin An optional edit origin that's passed through to each replaceRange(). * @return {Array<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean}>} * The list of passed selections adjusted for the performed edits, if any. */ Document.prototype.doMultipleEdits = function (edits, origin) { var self = this; // Sort the edits backwards, so we don't have to adjust the edit positions as we go along // (though we do have to adjust the selection positions). edits.sort(function (editDesc1, editDesc2) { var edit1 = (Array.isArray(editDesc1.edit) ? editDesc1.edit[0] : editDesc1.edit), edit2 = (Array.isArray(editDesc2.edit) ? editDesc2.edit[0] : editDesc2.edit); // Treat all no-op edits as if they should happen before all other edits (the order // doesn't really matter, as long as they sort out of the way of the real edits). if (!edit1) { return -1; } else if (!edit2) { return 1; } else { return CodeMirror.cmpPos(edit2.start, edit1.start); } }); // Pull out the selections, in the same order as the edits. var result = _.cloneDeep(_.pluck(edits, "selection")); // Preflight the edits to specify "end" if unspecified and make sure they don't overlap. // (We don't want to do it during the actual edits, since we don't want to apply some of // the edits before we find out.) _.each(edits, function (editDesc, index) { oneOrEach(editDesc.edit, function (edit) { if (edit) { if (!edit.end) { edit.end = edit.start; } if (index > 0) { var prevEditGroup = edits[index - 1].edit; // The edits are in reverse order, so we want to make sure this edit ends // before any of the previous ones start. oneOrEach(prevEditGroup, function (prevEdit) { if (CodeMirror.cmpPos(edit.end, prevEdit.start) > 0) { throw new Error("Document.doMultipleEdits(): Overlapping edits specified"); } }); } } }); }); // Perform the edits. this.batchOperation(function () { _.each(edits, function (editDesc, index) { // Perform this group of edits. The edit positions are guaranteed to be okay // since all the previous edits we've done have been later in the document. However, // we have to fix up any selections that overlap or come after the edit. oneOrEach(editDesc.edit, function (edit) { if (edit) { self.replaceRange(edit.text, edit.start, edit.end, origin); // Fix up all the selections *except* the one(s) related to this edit list that // are not "before-edit" selections. var textLines = edit.text.split("\n"); _.each(result, function (selections, selIndex) { if (selections) { oneOrEach(selections, function (sel) { if (sel.isBeforeEdit || selIndex !== index) { sel.start = self.adjustPosForChange(sel.start, textLines, edit.start, edit.end); sel.end = self.adjustPosForChange(sel.end, textLines, edit.start, edit.end); } }); } }); } }); }); }); result = _.chain(result) .filter(function (item) { return item !== undefined; }) .flatten() .sort(function (sel1, sel2) { return CodeMirror.cmpPos(sel1.start, sel2.start); }) .value(); _.each(result, function (item) { delete item.isBeforeEdit; }); return result; }; /* (pretty toString(), to aid debugging) */ Document.prototype.toString = function () { var dirtyInfo = (this.isDirty ? " (dirty!)" : " (clean)"); var editorInfo = (this._masterEditor ? " (Editable)" : " (Non-editable)"); var refInfo = " refs:" + this._refCount; return "[Document " + this.file.fullPath + dirtyInfo + editorInfo + refInfo + "]"; }; /** * Returns the language this document is written in. * The language returned is based on the file extension. * @return {Language} An object describing the language used in this document */ Document.prototype.getLanguage = function () { return this.language; }; /** * Updates the language to match the current mapping given by LanguageManager */ Document.prototype._updateLanguage = function () { var oldLanguage = this.language; this.language = LanguageManager.getLanguageForPath(this.file.fullPath); if (oldLanguage && oldLanguage !== this.language) { this.trigger("languageChanged", oldLanguage, this.language); } }; /** Called when Document.file has been modified (due to a rename) */ Document.prototype._notifyFilePathChanged = function () { // File extension may have changed this._updateLanguage(); }; /** * Is this an untitled document? * * @return {boolean} - whether or not the document is untitled */ Document.prototype.isUntitled = function () { return this.file instanceof InMemoryFile; }; // We dispatch events from the module level, and the instance level. Instance events are wired up // in the Document constructor. EventDispatcher.makeEventDispatcher(exports); // Define public API exports.Document = Document; });