diff --git a/src/document/Document.js b/src/document/Document.js index 31a36b0b696..33ba006f4a2 100644 --- a/src/document/Document.js +++ b/src/document/Document.js @@ -83,6 +83,11 @@ define(function (require, exports, module) { EventDispatcher.makeEventDispatcher(Document.prototype); + /** + * List of editors which were initialized as master editors for this doc. + */ + Document.prototype._associatedFullEditors = []; + /** * Number of clients who want this Document to stay alive. The Document is listed in * DocumentManager._openDocuments whenever refCount > 0. @@ -196,12 +201,16 @@ define(function (require, exports, module) { */ Document.prototype._makeEditable = function (masterEditor) { if (this._masterEditor) { - console.error("Document is already editable"); - } else { - this._text = null; - this._masterEditor = masterEditor; - masterEditor.on("change", this._handleEditorChange.bind(this)); + //Already a master editor is associated , so preserve the old editor in list of full editors + if (this._associatedFullEditors.indexOf(this._masterEditor) < 0) { + this._associatedFullEditors.push(this._masterEditor); + } } + + this._text = null; + this._masterEditor = masterEditor; + + masterEditor.on("change", this._handleEditorChange.bind(this)); }; /** @@ -215,7 +224,42 @@ define(function (require, exports, module) { } else { // _text represents the raw text, so fetch without normalized line endings this._text = this.getText(true); - this._masterEditor = null; + 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._associatedFullEditors.indexOf(masterEditor) >= 0) { + if (this._masterEditor) { + // Already a master editor is associated , so preserve the old editor in list of editors + if (this._associatedFullEditors.indexOf(this._masterEditor) < 0) { + this._associatedFullEditors.push(this._masterEditor); + } + } + this._masterEditor = masterEditor; + } + }; + + /** + * 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); } }; @@ -409,6 +453,11 @@ define(function (require, exports, module) { * @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. diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 2f0d88c840d..dba78974dcb 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -1115,6 +1115,7 @@ define(function (require, exports, module) { var file, promptOnly, _forceClose, + _spawnedRequest, paneId = MainViewManager.ACTIVE_PANE; if (commandData) { @@ -1122,6 +1123,7 @@ define(function (require, exports, module) { promptOnly = commandData.promptOnly; _forceClose = commandData._forceClose; paneId = commandData.paneId || paneId; + _spawnedRequest = commandData.spawnedRequest || false; } // utility function for handleFileClose: closes document & removes from workingset @@ -1146,8 +1148,9 @@ define(function (require, exports, module) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); - if (doc && doc.isDirty && !_forceClose) { - // Document is dirty: prompt to save changes before closing + if (doc && doc.isDirty && !_forceClose && (MainViewManager.isExclusiveToPane(doc.file, paneId) || _spawnedRequest)) { + // Document is dirty: prompt to save changes before closing if only the document is exclusively + // listed in the requested pane or this is part of a list close request var filename = FileUtils.getBaseName(doc.file.fullPath); Dialogs.showModalDialog( @@ -1245,7 +1248,7 @@ define(function (require, exports, module) { } else if (unsavedDocs.length === 1) { // Only one unsaved file: show the usual single-file-close confirmation UI - var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: promptOnly }; + var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: promptOnly, spawnedRequest: true }; handleFileClose(fileCloseArgs).done(function () { // still need to close any other, non-unsaved documents diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 264681127f4..52e1c440cc4 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -586,6 +586,9 @@ define(function (require, exports, module) { exports.trigger("documentRefreshed", doc); }) .on("_dirtyFlagChange", function (event, doc) { + // Modules listening on the doc instance notified about dirtyflag change + // To be used internally by Editor + doc.trigger("_dirtyFlagChange", doc); exports.trigger("dirtyFlagChange", doc); if (doc.isDirty) { MainViewManager.addToWorkingSet(MainViewManager.ACTIVE_PANE, doc.file); diff --git a/src/editor/Editor.js b/src/editor/Editor.js index 4d72406468d..3a0b4d6e760 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -80,6 +80,7 @@ define(function (require, exports, module) { TokenUtils = require("utils/TokenUtils"), ValidationUtils = require("utils/ValidationUtils"), ViewUtils = require("utils/ViewUtils"), + MainViewManager = require("view/MainViewManager"), _ = require("thirdparty/lodash"); /** Editor preferences */ @@ -310,9 +311,12 @@ define(function (require, exports, module) { this._handleDocumentChange = this._handleDocumentChange.bind(this); this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this); this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this); + this._doWorkingSetSync = this._doWorkingSetSync.bind(this); document.on("change", this._handleDocumentChange); document.on("deleted", this._handleDocumentDeleted); document.on("languageChanged", this._handleDocumentLanguageChanged); + // To sync working sets if the view is for same doc across panes + document.on("_dirtyFlagChange", this._doWorkingSetSync); var mode = this._getModeFromDocument(); @@ -325,6 +329,9 @@ define(function (require, exports, module) { this._$messagePopover = null; + // To track which pane the editor is being attached to if it's a full editor + this._paneId = null; + // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { "Tab": function () { self._handleTabKey(); }, @@ -449,6 +456,20 @@ define(function (require, exports, module) { EventDispatcher.makeEventDispatcher(Editor.prototype); EventDispatcher.markDeprecated(Editor.prototype, "keyEvent", "'keydown/press/up'"); + Editor.prototype.markPaneId = function (paneId) { + this._paneId = paneId; + // In case this Editor is initialized not as the first full editor for the document + // and the document is already dirty and present in another working set, make sure + // to add this documents to the new panes working set. + this._doWorkingSetSync(null, this.document); + }; + + Editor.prototype._doWorkingSetSync = function (event, doc) { + if (doc === this.document && this._paneId && this.document.isDirty) { + MainViewManager.addToWorkingSet(this._paneId, this.document.file, -1, false); + } + }; + /** * Removes this editor from the DOM and detaches from the Document. If this is the "master" * Editor that is secretly providing the Document's backing state, then the Document reverts to @@ -468,6 +489,7 @@ define(function (require, exports, module) { this.document.off("change", this._handleDocumentChange); this.document.off("deleted", this._handleDocumentDeleted); this.document.off("languageChanged", this._handleDocumentLanguageChanged); + this.document.off("_dirtyFlagChange", this._doWorkingSetSync); if (this._visibleRange) { // TextRange also refs the Document this._visibleRange.dispose(); @@ -476,6 +498,8 @@ define(function (require, exports, module) { // If we're the Document's master editor, disconnecting from it has special meaning if (this.document._masterEditor === this) { this.document._makeNonEditable(); + } else { + this.document._disassociateEditor(this); } // Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks @@ -961,6 +985,8 @@ define(function (require, exports, module) { this._codeMirror.on("focus", function () { self._focused = true; self.trigger("focus", self); + // Set this full editor as master editor for the document + self.document._toggleMasterEditor(self); }); this._codeMirror.on("blur", function () { diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index abaa5450759..6676d805195 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -542,7 +542,9 @@ define(function (require, exports, module) { var createdNewEditor = false, editor = document._masterEditor; - if (!editor) { + //Check if a master editor is not set already or the current master editor doesn't belong + //to the pane container requested - to support creation of multiple full editors + if (!editor || editor._paneId !== pane.id) { // Performance (see #4757) Chrome wastes time messing with selection // that will just be changed at end, so clear it for now if (window.getSelection && window.getSelection().empty) { // Chrome @@ -552,10 +554,6 @@ define(function (require, exports, module) { // Editor doesn't exist: populate a new Editor with the text editor = _createFullEditorForDocument(document, pane, editorOptions); createdNewEditor = true; - } else if (editor.$el.parent()[0] !== pane.$content[0]) { - // editor does exist but is not a child of the pane so add it to the - // pane (which will switch the view's container as well) - pane.addView(editor); } // show the view diff --git a/src/project/FileViewController.js b/src/project/FileViewController.js index 009bf29af73..4bb7d3b7d47 100644 --- a/src/project/FileViewController.js +++ b/src/project/FileViewController.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, $ */ +/*global define, $, event */ /** * Responsible for coordinating file selection between views by permitting only one view @@ -148,6 +148,19 @@ define(function (require, exports, module) { function openAndSelectDocument(fullPath, fileSelectionFocus, paneId) { var result, curDocChangedDueToMe = _curDocChangedDueToMe; + + function _getDerivedPaneContext() { + + function _secondPaneContext() { + return (event.ctrlKey || event.metaKey) && event.altKey ? MainViewManager.SECOND_PANE : null; + } + + function _firstPaneContext() { + return (event.ctrlKey || event.metaKey) ? MainViewManager.FIRST_PANE : null; + } + + return _secondPaneContext() || _firstPaneContext(); + } if (fileSelectionFocus !== PROJECT_MANAGER && fileSelectionFocus !== WORKING_SET_VIEW) { console.error("Bad parameter passed to FileViewController.openAndSelectDocument"); @@ -160,8 +173,9 @@ define(function (require, exports, module) { _curDocChangedDueToMe = true; _fileSelectionFocus = fileSelectionFocus; + - paneId = (paneId || MainViewManager.ACTIVE_PANE); + paneId = (paneId || _getDerivedPaneContext() || MainViewManager.ACTIVE_PANE); // If fullPath corresonds to the current doc being viewed then opening the file won't // trigger a currentFileChange event, so we need to trigger a documentSelectionFocusChange diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index 17e8c1e1ea3..9734beb506b 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -765,7 +765,7 @@ define(function (require, exports, module) { if (tryClosing || e.which === MIDDLE_BUTTON) { CommandManager .execute(Commands.FILE_CLOSE, {file: sourceFile, - paneId: sourceView.paneId}) + paneId: sourceView.paneId}) .always(function () { postDropCleanup(); }); diff --git a/src/view/MainViewManager.js b/src/view/MainViewManager.js index bdd8df3908c..63909215cf8 100644 --- a/src/view/MainViewManager.js +++ b/src/view/MainViewManager.js @@ -86,8 +86,6 @@ define(function (require, exports, module) { MainViewFactory = require("view/MainViewFactory"), ViewStateManager = require("view/ViewStateManager"), Commands = require("command/Commands"), - Dialogs = require("widgets/Dialogs"), - DefaultDialogs = require("widgets/DefaultDialogs"), EditorManager = require("editor/EditorManager"), FileSystemError = require("filesystem/FileSystemError"), DocumentManager = require("document/DocumentManager"), @@ -255,15 +253,29 @@ define(function (require, exports, module) { } /** - * Locates the first MRU entry of a file + * Locates the first MRU entry of a file for the requested pane + * @param {!string} paneId - the paneId * @param {!File} File - the file * @return {{file:File, paneId:string}} * @private */ - function _findFileInMRUList(file) { + function _findFileInMRUList(paneId, file) { return _.findIndex(_mruList, function (record) { - return (record.file.fullPath === file.fullPath); + return (record.file.fullPath === file.fullPath && record.paneId === paneId); + }); + } + + /** + * Checks whether a file is listed exclusively in the provided pane + * @param {!File} File - the file + * @return {{file:File, paneId:string}} + */ + function isExclusiveToPane(file, paneId) { + paneId = paneId === ACTIVE_PANE && _activePaneId ? _activePaneId : paneId; + var index = _.findIndex(_mruList, function (record) { + return (record.file.fullPath === file.fullPath && record.paneId !== paneId); }); + return index === -1; } @@ -343,7 +355,7 @@ define(function (require, exports, module) { _mruList.splice(index, 1); } - if (_findFileInMRUList(file) !== -1) { + if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } @@ -700,21 +712,10 @@ define(function (require, exports, module) { */ function addToWorkingSet(paneId, file, index, force) { // look for the file to have already been added to another pane - var pane = _getPane(paneId), - existingPaneId = _getPaneIdForPath(file.fullPath); - + var pane = _getPane(paneId); if (!pane) { throw new Error("invalid pane id: " + paneId); } - - if (findInWorkingSet(ALL_PANES, file.fullPath) !== -1) { - return; - } - - // if it's already open in another pane, then just use that pane - if (existingPaneId && existingPaneId !== pane.id) { - pane = _getPane(existingPaneId); - } var result = pane.reorderItem(file, index, force), entry = _makeMRUListEntry(file, pane.id); @@ -728,7 +729,7 @@ define(function (require, exports, module) { } else if (result === pane.ITEM_NOT_FOUND) { index = pane.addToViewList(file, index); - if (_findFileInMRUList(file) === -1) { + if (_findFileInMRUList(pane.id, file) === -1) { // Add to or update the position in MRU if (pane.getCurrentlyViewedFile() === file) { _mruList.unshift(entry); @@ -753,7 +754,7 @@ define(function (require, exports, module) { uniqueFileList = pane.addListToViewList(fileList); uniqueFileList.forEach(function (file) { - if (_findFileInMRUList(file) !== -1) { + if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } _mruList.push(_makeMRUListEntry(file, pane.id)); @@ -1185,16 +1186,7 @@ define(function (require, exports, module) { * @private */ function _edit(paneId, doc, optionsIn) { - var options = optionsIn || {}, - currentPaneId = _getPaneIdForPath(doc.file.fullPath); - - if (currentPaneId) { - // If the doc is open in another pane then switch to that pane and call open document - // which will really just show the view as it has always done we could just - // do pane.showView(doc._masterEditor) in that case but Editor Manager may do some - // state syncing - paneId = currentPaneId; - } + var options = optionsIn || {}; var pane = _getPane(paneId); @@ -1236,32 +1228,6 @@ define(function (require, exports, module) { return result.reject("bad argument").promise(); } - var currentPaneId = _getPaneIdForPath(file.fullPath); - - if (currentPaneId) { - // Warn user (only once) when file is already open in another view - if (!PreferencesManager.getViewState("splitview.multipane-info") && - currentPaneId !== _resolvePaneId(paneId)) { - PreferencesManager.setViewState("splitview.multipane-info", true); - - // File tree also executes single-click code prior to executing double-click - // code, so delay showing modal dialog to prevent eating second click - window.setTimeout(function () { - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - Strings.SPLITVIEW_INFO_TITLE, - Strings.SPLITVIEW_MULTIPANE_WARNING - ); - }, 500); - } - - // If the doc is open in another pane - // then switch to that pane and call open document - // which will really just show the view as it has always done - // we could just do pane.showView(doc._masterEditor) in that - // case but Editor Manager may do some state syncing - paneId = currentPaneId; - } // See if there is already a view for the file var pane = _getPane(paneId); @@ -1371,7 +1337,7 @@ define(function (require, exports, module) { function _close(paneId, file, optionsIn) { var options = optionsIn || {}; _forEachPaneOrPanes(paneId, function (pane) { - if (pane.removeView(file, options.noOpenNextFile)) { + if (pane.removeView(file, options.noOpenNextFile) && pane.id === paneId) { _removeFileFromMRU(pane.id, file); exports.trigger("workingSetRemove", file, false, pane.id); return false; @@ -1556,7 +1522,7 @@ define(function (require, exports, module) { var fileList = pane.getViewList(); fileList.forEach(function (file) { - if (_findFileInMRUList(file) !== -1) { + if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } _mruList.push(_makeMRUListEntry(file, pane.id)); @@ -1760,6 +1726,7 @@ define(function (require, exports, module) { exports.getPaneIdList = getPaneIdList; exports.getPaneTitle = getPaneTitle; exports.getPaneCount = getPaneCount; + exports.isExclusiveToPane = isExclusiveToPane; exports.getAllOpenFiles = getAllOpenFiles; exports.focusActivePane = focusActivePane; @@ -1775,4 +1742,6 @@ define(function (require, exports, module) { // Constants exports.ALL_PANES = ALL_PANES; exports.ACTIVE_PANE = ACTIVE_PANE; + exports.FIRST_PANE = FIRST_PANE; + exports.SECOND_PANE = SECOND_PANE; }); diff --git a/src/view/Pane.js b/src/view/Pane.js index 3422cd295b6..b17a06d8c31 100644 --- a/src/view/Pane.js +++ b/src/view/Pane.js @@ -263,7 +263,7 @@ define(function (require, exports, module) { var file = self.getCurrentlyViewedFile(); if (file) { - CommandManager.execute(Commands.FILE_CLOSE, {File: file}); + CommandManager.execute(Commands.FILE_CLOSE, {File: file, paneId: self.id}); if (!self.getCurrentlyViewedFile() && PreferencesManager.get("pane.mergePanesWhenLastFileClosed")) { MainViewManager.setLayoutScheme(1, 1); @@ -773,7 +773,7 @@ define(function (require, exports, module) { */ Pane.prototype._canAddFile = function (file) { return ((this._views.hasOwnProperty(file.fullPath) && this.findInViewList(file.fullPath) === -1) || - (!MainViewManager._getPaneIdForPath(file.fullPath))); + (MainViewManager._getPaneIdForPath(file.fullPath) !== this.id)); }; /** @@ -1110,7 +1110,11 @@ define(function (require, exports, module) { } else { this._views[path] = view; } - + + // Ensure that we don't endup marking the custom views + if (view.markPaneId) { + view.markPaneId(this.id); + } if (show) { this.showView(view); diff --git a/test/spec/EditorManager-test.js b/test/spec/EditorManager-test.js index e8518e87101..aea9953bc6a 100644 --- a/test/spec/EditorManager-test.js +++ b/test/spec/EditorManager-test.js @@ -33,7 +33,7 @@ define(function (require, exports, module) { SpecRunnerUtils = require("spec/SpecRunnerUtils"); describe("EditorManager", function () { - var pane, testEditor, testDoc, $fakeContentDiv, $fakeHolder; + var pane, anotherPane, testEditor, testDoc, $fakeContentDiv, $fakeHolder; beforeEach(function () { // Normally the editor holder would be created inside a "content" div, which is // used in the available height calculation. We create a fake content div just to @@ -47,7 +47,8 @@ define(function (require, exports, module) { .attr("id", "hidden-editors") .appendTo($fakeContentDiv); - pane = SpecRunnerUtils.createMockPane($fakeHolder); + pane = SpecRunnerUtils.createMockPane($fakeHolder, 'first-pane'); + anotherPane = SpecRunnerUtils.createMockPane($fakeHolder, 'second-pane'); testDoc = SpecRunnerUtils.createMockDocument(""); }); afterEach(function () { @@ -66,19 +67,32 @@ define(function (require, exports, module) { describe("Create Editors", function () { - it("should create a new editor for a document and add it to a pane", function () { + it("should create a new editor for a document and add it to the requested pane", function () { spyOn(pane, "addView"); EditorManager.openDocument(testDoc, pane); expect(pane.addView).toHaveBeenCalled(); }); - it("should use an existing editor for a document and show the editor", function () { + it("should use an existing editor for a document when requested on same pane", function () { spyOn(pane, "addView"); spyOn(pane, "showView"); - var editor = SpecRunnerUtils.createEditorInstance(testDoc, pane.$el); + var editor = SpecRunnerUtils.createEditorInstance(testDoc, pane.$el, undefined, 'first-pane'); EditorManager.openDocument(testDoc, pane); - expect(pane.addView).toHaveBeenCalled(); - expect(pane.showView).toHaveBeenCalled(); - expect(pane.addView.calls[0].args[0]).toEqual(editor); + expect(pane.addView).not.toHaveBeenCalled(); + expect(pane.showView).toHaveBeenCalledWith(editor); + }); + it("should create a new editor for a document if requested pane and existing editors pane is not same", function () { + spyOn(pane, "addView"); + spyOn(pane, "showView"); + spyOn(anotherPane, "addView"); + spyOn(anotherPane, "showView"); + var editor = SpecRunnerUtils.createEditorInstance(testDoc, anotherPane.$el, undefined, 'first-pane'); + EditorManager.openDocument(testDoc, anotherPane); + expect(pane.addView).not.toHaveBeenCalled(); + expect(pane.showView).not.toHaveBeenCalled(); + expect(anotherPane.addView).toHaveBeenCalled(); + expect(anotherPane.addView).not.toHaveBeenCalledWith(editor); + expect(anotherPane.showView).toHaveBeenCalled(); + expect(anotherPane.showView).not.toHaveBeenCalledWith(editor); }); }); }); diff --git a/test/spec/Pane-test.js b/test/spec/Pane-test.js index 1b6336e840e..0d343e1cdcd 100644 --- a/test/spec/Pane-test.js +++ b/test/spec/Pane-test.js @@ -70,7 +70,11 @@ define(function (require, exports, module) { }, notifyVisibilityChange: function (visible) { this._visible = visible; + }, + markPaneId: function (id) { + this._paneId = id; } + }; } diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 117547128fc..85ab466c991 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -395,11 +395,14 @@ define(function (require, exports, module) { .appendTo($("body")); } - function createEditorInstance(doc, $editorHolder, visibleRange) { + function createEditorInstance(doc, $editorHolder, visibleRange, paneId) { var editor = new Editor(doc, true, $editorHolder.get(0), visibleRange); Editor.setUseTabChar(EDITOR_USE_TABS); Editor.setSpaceUnits(EDITOR_SPACE_UNITS); + if (paneId) { + editor._paneId = paneId; + } EditorManager._notifyActiveEditorChanged(editor); return editor; @@ -445,7 +448,7 @@ define(function (require, exports, module) { return { doc: doc, editor: createMockEditorForDocument(doc, visibleRange) }; } - function createMockPane($el) { + function createMockPane($el, paneId) { createMockElement() .attr("class", "pane-header") .appendTo($el); @@ -455,6 +458,7 @@ define(function (require, exports, module) { return { $el: $el, + id: paneId || 'first-pane', $content: $fakeContent, addView: function (path, editor) { },