StyleSheetEditor.jsm 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. /* vim:set ts=2 sw=2 sts=2 et: */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
  7. const Cc = Components.classes;
  8. const Ci = Components.interfaces;
  9. const Cu = Components.utils;
  10. const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  11. const Editor = require("devtools/client/sourceeditor/editor");
  12. const promise = require("promise");
  13. const defer = require("devtools/shared/defer");
  14. const {shortSource, prettifyCSS} = require("devtools/shared/inspector/css-logic");
  15. const {console} = require("resource://gre/modules/Console.jsm");
  16. const Services = require("Services");
  17. const EventEmitter = require("devtools/shared/event-emitter");
  18. const {Task} = require("devtools/shared/task");
  19. const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
  20. const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
  21. const {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
  22. const {
  23. getString,
  24. showFilePicker,
  25. } = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
  26. const LOAD_ERROR = "error-load";
  27. const SAVE_ERROR = "error-save";
  28. // max update frequency in ms (avoid potential typing lag and/or flicker)
  29. // @see StyleEditor.updateStylesheet
  30. const UPDATE_STYLESHEET_DELAY = 500;
  31. // Pref which decides if CSS autocompletion is enabled in Style Editor or not.
  32. const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
  33. // Pref which decides whether updates to the stylesheet use transitions
  34. const TRANSITION_PREF = "devtools.styleeditor.transitions";
  35. // How long to wait to update linked CSS file after original source was saved
  36. // to disk. Time in ms.
  37. const CHECK_LINKED_SHEET_DELAY = 500;
  38. // How many times to check for linked file changes
  39. const MAX_CHECK_COUNT = 10;
  40. // The classname used to show a line that is not used
  41. const UNUSED_CLASS = "cm-unused-line";
  42. // How much time should the mouse be still before the selector at that position
  43. // gets highlighted?
  44. const SELECTOR_HIGHLIGHT_TIMEOUT = 500;
  45. /**
  46. * StyleSheetEditor controls the editor linked to a particular StyleSheet
  47. * object.
  48. *
  49. * Emits events:
  50. * 'property-change': A property on the underlying stylesheet has changed
  51. * 'source-editor-load': The source editor for this editor has been loaded
  52. * 'error': An error has occured
  53. *
  54. * @param {StyleSheet|OriginalSource} styleSheet
  55. * Stylesheet or original source to show
  56. * @param {DOMWindow} win
  57. * panel window for style editor
  58. * @param {nsIFile} file
  59. * Optional file that the sheet was imported from
  60. * @param {boolean} isNew
  61. * Optional whether the sheet was created by the user
  62. * @param {Walker} walker
  63. * Optional walker used for selectors autocompletion
  64. * @param {CustomHighlighterFront} highlighter
  65. * Optional highlighter front for the SelectorHighligher used to
  66. * highlight selectors
  67. */
  68. function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
  69. EventEmitter.decorate(this);
  70. this.styleSheet = styleSheet;
  71. this._inputElement = null;
  72. this.sourceEditor = null;
  73. this._window = win;
  74. this._isNew = isNew;
  75. this.walker = walker;
  76. this.highlighter = highlighter;
  77. // True when we've called update() on the style sheet.
  78. this._isUpdating = false;
  79. // True when we've just set the editor text based on a style-applied
  80. // event from the StyleSheetActor.
  81. this._justSetText = false;
  82. // state to use when inputElement attaches
  83. this._state = {
  84. text: "",
  85. selection: {
  86. start: {line: 0, ch: 0},
  87. end: {line: 0, ch: 0}
  88. }
  89. };
  90. this._styleSheetFilePath = null;
  91. if (styleSheet.href &&
  92. Services.io.extractScheme(this.styleSheet.href) == "file") {
  93. this._styleSheetFilePath = this.styleSheet.href;
  94. }
  95. this._onPropertyChange = this._onPropertyChange.bind(this);
  96. this._onError = this._onError.bind(this);
  97. this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this);
  98. this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this);
  99. this._onStyleApplied = this._onStyleApplied.bind(this);
  100. this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
  101. this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
  102. this.saveToFile = this.saveToFile.bind(this);
  103. this.updateStyleSheet = this.updateStyleSheet.bind(this);
  104. this._updateStyleSheet = this._updateStyleSheet.bind(this);
  105. this._onMouseMove = this._onMouseMove.bind(this);
  106. this._focusOnSourceEditorReady = false;
  107. this.cssSheet.on("property-change", this._onPropertyChange);
  108. this.styleSheet.on("error", this._onError);
  109. this.mediaRules = [];
  110. if (this.cssSheet.getMediaRules) {
  111. this.cssSheet.getMediaRules().then(this._onMediaRulesChanged,
  112. e => console.error(e));
  113. }
  114. this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
  115. this.cssSheet.on("style-applied", this._onStyleApplied);
  116. this.savedFile = file;
  117. this.linkCSSFile();
  118. }
  119. this.StyleSheetEditor = StyleSheetEditor;
  120. StyleSheetEditor.prototype = {
  121. /**
  122. * Whether there are unsaved changes in the editor
  123. */
  124. get unsaved() {
  125. return this.sourceEditor && !this.sourceEditor.isClean();
  126. },
  127. /**
  128. * Whether the editor is for a stylesheet created by the user
  129. * through the style editor UI.
  130. */
  131. get isNew() {
  132. return this._isNew;
  133. },
  134. /**
  135. * The style sheet or the generated style sheet for this source if it's an
  136. * original source.
  137. */
  138. get cssSheet() {
  139. if (this.styleSheet.isOriginalSource) {
  140. return this.styleSheet.relatedStyleSheet;
  141. }
  142. return this.styleSheet;
  143. },
  144. get savedFile() {
  145. return this._savedFile;
  146. },
  147. set savedFile(name) {
  148. this._savedFile = name;
  149. this.linkCSSFile();
  150. },
  151. /**
  152. * Get a user-friendly name for the style sheet.
  153. *
  154. * @return string
  155. */
  156. get friendlyName() {
  157. if (this.savedFile) {
  158. return this.savedFile.leafName;
  159. }
  160. if (this._isNew) {
  161. let index = this.styleSheet.styleSheetIndex + 1;
  162. return getString("newStyleSheet", index);
  163. }
  164. if (!this.styleSheet.href) {
  165. let index = this.styleSheet.styleSheetIndex + 1;
  166. return getString("inlineStyleSheet", index);
  167. }
  168. if (!this._friendlyName) {
  169. let sheetURI = this.styleSheet.href;
  170. this._friendlyName = shortSource({ href: sheetURI });
  171. try {
  172. this._friendlyName = decodeURI(this._friendlyName);
  173. } catch (ex) {
  174. // Ignore.
  175. }
  176. }
  177. return this._friendlyName;
  178. },
  179. /**
  180. * Check if transitions are enabled for style changes.
  181. *
  182. * @return Boolean
  183. */
  184. get transitionsEnabled() {
  185. return Services.prefs.getBoolPref(TRANSITION_PREF);
  186. },
  187. /**
  188. * If this is an original source, get the path of the CSS file it generated.
  189. */
  190. linkCSSFile: function () {
  191. if (!this.styleSheet.isOriginalSource) {
  192. return;
  193. }
  194. let relatedSheet = this.styleSheet.relatedStyleSheet;
  195. if (!relatedSheet || !relatedSheet.href) {
  196. return;
  197. }
  198. let path;
  199. let href = removeQuery(relatedSheet.href);
  200. let uri = NetUtil.newURI(href);
  201. if (uri.scheme == "file") {
  202. let file = uri.QueryInterface(Ci.nsIFileURL).file;
  203. path = file.path;
  204. } else if (this.savedFile) {
  205. let origHref = removeQuery(this.styleSheet.href);
  206. let origUri = NetUtil.newURI(origHref);
  207. path = findLinkedFilePath(uri, origUri, this.savedFile);
  208. } else {
  209. // we can't determine path to generated file on disk
  210. return;
  211. }
  212. if (this.linkedCSSFile == path) {
  213. return;
  214. }
  215. this.linkedCSSFile = path;
  216. this.linkedCSSFileError = null;
  217. // save last file change time so we can compare when we check for changes.
  218. OS.File.stat(path).then((info) => {
  219. this._fileModDate = info.lastModificationDate.getTime();
  220. }, this.markLinkedFileBroken);
  221. this.emit("linked-css-file");
  222. },
  223. /**
  224. * A helper function that fetches the source text from the style
  225. * sheet. The text is possibly prettified using prettifyCSS. This
  226. * also sets |this._state.text| to the new text.
  227. *
  228. * @return {Promise} a promise that resolves to the new text
  229. */
  230. _getSourceTextAndPrettify: function () {
  231. return this.styleSheet.getText().then((longStr) => {
  232. return longStr.string();
  233. }).then((source) => {
  234. let ruleCount = this.styleSheet.ruleCount;
  235. if (!this.styleSheet.isOriginalSource) {
  236. source = prettifyCSS(source, ruleCount);
  237. }
  238. this._state.text = source;
  239. return source;
  240. });
  241. },
  242. /**
  243. * Start fetching the full text source for this editor's sheet.
  244. *
  245. * @return {Promise}
  246. * A promise that'll resolve with the source text once the source
  247. * has been loaded or reject on unexpected error.
  248. */
  249. fetchSource: function () {
  250. return this._getSourceTextAndPrettify().then((source) => {
  251. this.sourceLoaded = true;
  252. return source;
  253. }).then(null, e => {
  254. if (this._isDestroyed) {
  255. console.warn("Could not fetch the source for " +
  256. this.styleSheet.href +
  257. ", the editor was destroyed");
  258. console.error(e);
  259. } else {
  260. this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href });
  261. throw e;
  262. }
  263. });
  264. },
  265. /**
  266. * Add markup to a region. UNUSED_CLASS is added to specified lines
  267. * @param region An object shaped like
  268. * {
  269. * start: { line: L1, column: C1 },
  270. * end: { line: L2, column: C2 } // optional
  271. * }
  272. */
  273. addUnusedRegion: function (region) {
  274. this.sourceEditor.addLineClass(region.start.line - 1, UNUSED_CLASS);
  275. if (region.end) {
  276. for (let i = region.start.line; i <= region.end.line; i++) {
  277. this.sourceEditor.addLineClass(i - 1, UNUSED_CLASS);
  278. }
  279. }
  280. },
  281. /**
  282. * As addUnusedRegion except that it takes an array of regions
  283. */
  284. addUnusedRegions: function (regions) {
  285. for (let region of regions) {
  286. this.addUnusedRegion(region);
  287. }
  288. },
  289. /**
  290. * Remove all the unused markup regions added by addUnusedRegion
  291. */
  292. removeAllUnusedRegions: function () {
  293. for (let i = 0; i < this.sourceEditor.lineCount(); i++) {
  294. this.sourceEditor.removeLineClass(i, UNUSED_CLASS);
  295. }
  296. },
  297. /**
  298. * Forward property-change event from stylesheet.
  299. *
  300. * @param {string} event
  301. * Event type
  302. * @param {string} property
  303. * Property that has changed on sheet
  304. */
  305. _onPropertyChange: function (property, value) {
  306. this.emit("property-change", property, value);
  307. },
  308. /**
  309. * Called when the stylesheet text changes.
  310. */
  311. _onStyleApplied: function () {
  312. if (this._isUpdating) {
  313. // We just applied an edit in the editor, so we can drop this
  314. // notification.
  315. this._isUpdating = false;
  316. } else if (this.sourceEditor) {
  317. this._getSourceTextAndPrettify().then((newText) => {
  318. this._justSetText = true;
  319. let firstLine = this.sourceEditor.getFirstVisibleLine();
  320. let pos = this.sourceEditor.getCursor();
  321. this.sourceEditor.setText(newText);
  322. this.sourceEditor.setFirstVisibleLine(firstLine);
  323. this.sourceEditor.setCursor(pos);
  324. this.emit("style-applied");
  325. });
  326. }
  327. },
  328. /**
  329. * Handles changes to the list of @media rules in the stylesheet.
  330. * Emits 'media-rules-changed' if the list has changed.
  331. *
  332. * @param {array} rules
  333. * Array of MediaRuleFronts for new media rules of sheet.
  334. */
  335. _onMediaRulesChanged: function (rules) {
  336. if (!rules.length && !this.mediaRules.length) {
  337. return;
  338. }
  339. for (let rule of this.mediaRules) {
  340. rule.off("matches-change", this._onMediaRuleMatchesChange);
  341. rule.destroy();
  342. }
  343. this.mediaRules = rules;
  344. for (let rule of rules) {
  345. rule.on("matches-change", this._onMediaRuleMatchesChange);
  346. }
  347. this.emit("media-rules-changed", rules);
  348. },
  349. /**
  350. * Forward media-rules-changed event from stylesheet.
  351. */
  352. _onMediaRuleMatchesChange: function () {
  353. this.emit("media-rules-changed", this.mediaRules);
  354. },
  355. /**
  356. * Forward error event from stylesheet.
  357. *
  358. * @param {string} event
  359. * Event type
  360. * @param {string} errorCode
  361. */
  362. _onError: function (event, data) {
  363. this.emit("error", data);
  364. },
  365. /**
  366. * Create source editor and load state into it.
  367. * @param {DOMElement} inputElement
  368. * Element to load source editor in
  369. * @param {CssProperties} cssProperties
  370. * A css properties database.
  371. *
  372. * @return {Promise}
  373. * Promise that will resolve when the style editor is loaded.
  374. */
  375. load: function (inputElement, cssProperties) {
  376. if (this._isDestroyed) {
  377. return promise.reject("Won't load source editor as the style sheet has " +
  378. "already been removed from Style Editor.");
  379. }
  380. this._inputElement = inputElement;
  381. let config = {
  382. value: this._state.text,
  383. lineNumbers: true,
  384. mode: Editor.modes.css,
  385. readOnly: false,
  386. autoCloseBrackets: "{}()",
  387. extraKeys: this._getKeyBindings(),
  388. contextMenu: "sourceEditorContextMenu",
  389. autocomplete: Services.prefs.getBoolPref(AUTOCOMPLETION_PREF),
  390. autocompleteOpts: { walker: this.walker, cssProperties },
  391. cssProperties
  392. };
  393. let sourceEditor = this._sourceEditor = new Editor(config);
  394. sourceEditor.on("dirty-change", this._onPropertyChange);
  395. return sourceEditor.appendTo(inputElement).then(() => {
  396. sourceEditor.on("saveRequested", this.saveToFile);
  397. if (this.styleSheet.update) {
  398. sourceEditor.on("change", this.updateStyleSheet);
  399. }
  400. this.sourceEditor = sourceEditor;
  401. if (this._focusOnSourceEditorReady) {
  402. this._focusOnSourceEditorReady = false;
  403. sourceEditor.focus();
  404. }
  405. sourceEditor.setSelection(this._state.selection.start,
  406. this._state.selection.end);
  407. if (this.highlighter && this.walker) {
  408. sourceEditor.container.addEventListener("mousemove", this._onMouseMove);
  409. }
  410. // Add the commands controller for the source-editor.
  411. sourceEditor.insertCommandsController();
  412. this.emit("source-editor-load");
  413. });
  414. },
  415. /**
  416. * Get the source editor for this editor.
  417. *
  418. * @return {Promise}
  419. * Promise that will resolve with the editor.
  420. */
  421. getSourceEditor: function () {
  422. let deferred = defer();
  423. if (this.sourceEditor) {
  424. return promise.resolve(this);
  425. }
  426. this.on("source-editor-load", () => {
  427. deferred.resolve(this);
  428. });
  429. return deferred.promise;
  430. },
  431. /**
  432. * Focus the Style Editor input.
  433. */
  434. focus: function () {
  435. if (this.sourceEditor) {
  436. this.sourceEditor.focus();
  437. } else {
  438. this._focusOnSourceEditorReady = true;
  439. }
  440. },
  441. /**
  442. * Event handler for when the editor is shown.
  443. */
  444. onShow: function () {
  445. if (this.sourceEditor) {
  446. // CodeMirror needs refresh to restore scroll position after hiding and
  447. // showing the editor.
  448. this.sourceEditor.refresh();
  449. }
  450. this.focus();
  451. },
  452. /**
  453. * Toggled the disabled state of the underlying stylesheet.
  454. */
  455. toggleDisabled: function () {
  456. this.styleSheet.toggleDisabled().then(null, e => console.error(e));
  457. },
  458. /**
  459. * Queue a throttled task to update the live style sheet.
  460. */
  461. updateStyleSheet: function () {
  462. if (this._updateTask) {
  463. // cancel previous queued task not executed within throttle delay
  464. this._window.clearTimeout(this._updateTask);
  465. }
  466. this._updateTask = this._window.setTimeout(this._updateStyleSheet,
  467. UPDATE_STYLESHEET_DELAY);
  468. },
  469. /**
  470. * Update live style sheet according to modifications.
  471. */
  472. _updateStyleSheet: function () {
  473. if (this.styleSheet.disabled) {
  474. // TODO: do we want to do this?
  475. return;
  476. }
  477. if (this._justSetText) {
  478. this._justSetText = false;
  479. return;
  480. }
  481. // reset only if we actually perform an update
  482. // (stylesheet is enabled) so that 'missed' updates
  483. // while the stylesheet is disabled can be performed
  484. // when it is enabled back. @see enableStylesheet
  485. this._updateTask = null;
  486. if (this.sourceEditor) {
  487. this._state.text = this.sourceEditor.getText();
  488. }
  489. this._isUpdating = true;
  490. this.styleSheet.update(this._state.text, this.transitionsEnabled)
  491. .then(null, e => console.error(e));
  492. },
  493. /**
  494. * Handle mousemove events, calling _highlightSelectorAt after a delay only
  495. * and reseting the delay everytime.
  496. */
  497. _onMouseMove: function (e) {
  498. this.highlighter.hide();
  499. if (this.mouseMoveTimeout) {
  500. this._window.clearTimeout(this.mouseMoveTimeout);
  501. this.mouseMoveTimeout = null;
  502. }
  503. this.mouseMoveTimeout = this._window.setTimeout(() => {
  504. this._highlightSelectorAt(e.clientX, e.clientY);
  505. }, SELECTOR_HIGHLIGHT_TIMEOUT);
  506. },
  507. /**
  508. * Highlight nodes matching the selector found at coordinates x,y in the
  509. * editor, if any.
  510. *
  511. * @param {Number} x
  512. * @param {Number} y
  513. */
  514. _highlightSelectorAt: Task.async(function* (x, y) {
  515. let pos = this.sourceEditor.getPositionFromCoords({left: x, top: y});
  516. let info = this.sourceEditor.getInfoAt(pos);
  517. if (!info || info.state !== "selector") {
  518. return;
  519. }
  520. let node =
  521. yield this.walker.getStyleSheetOwnerNode(this.styleSheet.actorID);
  522. yield this.highlighter.show(node, {
  523. selector: info.selector,
  524. hideInfoBar: true,
  525. showOnly: "border",
  526. region: "border"
  527. });
  528. this.emit("node-highlighted");
  529. }),
  530. /**
  531. * Save the editor contents into a file and set savedFile property.
  532. * A file picker UI will open if file is not set and editor is not headless.
  533. *
  534. * @param mixed file
  535. * Optional nsIFile or string representing the filename to save in the
  536. * background, no UI will be displayed.
  537. * If not specified, the original style sheet URI is used.
  538. * To implement 'Save' instead of 'Save as', you can pass
  539. * savedFile here.
  540. * @param function(nsIFile aFile) callback
  541. * Optional callback called when the operation has finished.
  542. * aFile has the nsIFile object for saved file or null if the operation
  543. * has failed or has been canceled by the user.
  544. * @see savedFile
  545. */
  546. saveToFile: function (file, callback) {
  547. let onFile = (returnFile) => {
  548. if (!returnFile) {
  549. if (callback) {
  550. callback(null);
  551. }
  552. return;
  553. }
  554. if (this.sourceEditor) {
  555. this._state.text = this.sourceEditor.getText();
  556. }
  557. let ostream = FileUtils.openSafeFileOutputStream(returnFile);
  558. let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  559. .createInstance(Ci.nsIScriptableUnicodeConverter);
  560. converter.charset = "UTF-8";
  561. let istream = converter.convertToInputStream(this._state.text);
  562. NetUtil.asyncCopy(istream, ostream, (status) => {
  563. if (!Components.isSuccessCode(status)) {
  564. if (callback) {
  565. callback(null);
  566. }
  567. this.emit("error", { key: SAVE_ERROR });
  568. return;
  569. }
  570. FileUtils.closeSafeFileOutputStream(ostream);
  571. this.onFileSaved(returnFile);
  572. if (callback) {
  573. callback(returnFile);
  574. }
  575. });
  576. };
  577. let defaultName;
  578. if (this._friendlyName) {
  579. defaultName = OS.Path.basename(this._friendlyName);
  580. }
  581. showFilePicker(file || this._styleSheetFilePath, true, this._window,
  582. onFile, defaultName);
  583. },
  584. /**
  585. * Called when this source has been successfully saved to disk.
  586. */
  587. onFileSaved: function (returnFile) {
  588. this._friendlyName = null;
  589. this.savedFile = returnFile;
  590. if (this.sourceEditor) {
  591. this.sourceEditor.setClean();
  592. }
  593. this.emit("property-change");
  594. // TODO: replace with file watching
  595. this._modCheckCount = 0;
  596. this._window.clearTimeout(this._timeout);
  597. if (this.linkedCSSFile && !this.linkedCSSFileError) {
  598. this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
  599. CHECK_LINKED_SHEET_DELAY);
  600. }
  601. },
  602. /**
  603. * Check to see if our linked CSS file has changed on disk, and
  604. * if so, update the live style sheet.
  605. */
  606. checkLinkedFileForChanges: function () {
  607. OS.File.stat(this.linkedCSSFile).then((info) => {
  608. let lastChange = info.lastModificationDate.getTime();
  609. if (this._fileModDate && lastChange != this._fileModDate) {
  610. this._fileModDate = lastChange;
  611. this._modCheckCount = 0;
  612. this.updateLinkedStyleSheet();
  613. return;
  614. }
  615. if (++this._modCheckCount > MAX_CHECK_COUNT) {
  616. this.updateLinkedStyleSheet();
  617. return;
  618. }
  619. // try again in a bit
  620. this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
  621. CHECK_LINKED_SHEET_DELAY);
  622. }, this.markLinkedFileBroken);
  623. },
  624. /**
  625. * Notify that the linked CSS file (if this is an original source)
  626. * doesn't exist on disk in the place we think it does.
  627. *
  628. * @param string error
  629. * The error we got when trying to access the file.
  630. */
  631. markLinkedFileBroken: function (error) {
  632. this.linkedCSSFileError = error || true;
  633. this.emit("linked-css-file-error");
  634. error += " querying " + this.linkedCSSFile +
  635. " original source location: " + this.savedFile.path;
  636. console.error(error);
  637. },
  638. /**
  639. * For original sources (e.g. Sass files). Fetch contents of linked CSS
  640. * file from disk and live update the stylesheet object with the contents.
  641. */
  642. updateLinkedStyleSheet: function () {
  643. OS.File.read(this.linkedCSSFile).then((array) => {
  644. let decoder = new TextDecoder();
  645. let text = decoder.decode(array);
  646. let relatedSheet = this.styleSheet.relatedStyleSheet;
  647. relatedSheet.update(text, this.transitionsEnabled);
  648. }, this.markLinkedFileBroken);
  649. },
  650. /**
  651. * Retrieve custom key bindings objects as expected by Editor.
  652. * Editor action names are not displayed to the user.
  653. *
  654. * @return {array} key binding objects for the source editor
  655. */
  656. _getKeyBindings: function () {
  657. let bindings = {};
  658. let keybind = Editor.accel(getString("saveStyleSheet.commandkey"));
  659. bindings[keybind] = () => {
  660. this.saveToFile(this.savedFile);
  661. };
  662. bindings["Shift-" + keybind] = () => {
  663. this.saveToFile();
  664. };
  665. bindings.Esc = false;
  666. return bindings;
  667. },
  668. /**
  669. * Clean up for this editor.
  670. */
  671. destroy: function () {
  672. if (this._sourceEditor) {
  673. this._sourceEditor.off("dirty-change", this._onPropertyChange);
  674. this._sourceEditor.off("saveRequested", this.saveToFile);
  675. this._sourceEditor.off("change", this.updateStyleSheet);
  676. if (this.highlighter && this.walker && this._sourceEditor.container) {
  677. this._sourceEditor.container.removeEventListener("mousemove",
  678. this._onMouseMove);
  679. }
  680. this._sourceEditor.destroy();
  681. }
  682. this.cssSheet.off("property-change", this._onPropertyChange);
  683. this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
  684. this.cssSheet.off("style-applied", this._onStyleApplied);
  685. this.styleSheet.off("error", this._onError);
  686. this._isDestroyed = true;
  687. }
  688. };
  689. /**
  690. * Find a path on disk for a file given it's hosted uri, the uri of the
  691. * original resource that generated it (e.g. Sass file), and the location of the
  692. * local file for that source.
  693. *
  694. * @param {nsIURI} uri
  695. * The uri of the resource
  696. * @param {nsIURI} origUri
  697. * The uri of the original source for the resource
  698. * @param {nsIFile} file
  699. * The local file for the resource on disk
  700. *
  701. * @return {string}
  702. * The path of original file on disk
  703. */
  704. function findLinkedFilePath(uri, origUri, file) {
  705. let { origBranch, branch } = findUnsharedBranches(origUri, uri);
  706. let project = findProjectPath(file, origBranch);
  707. let parts = project.concat(branch);
  708. let path = OS.Path.join.apply(this, parts);
  709. return path;
  710. }
  711. /**
  712. * Find the path of a project given a file in the project and its branch
  713. * off the root. e.g.:
  714. * /Users/moz/proj/src/a.css" and "src/a.css"
  715. * would yield ["Users", "moz", "proj"]
  716. *
  717. * @param {nsIFile} file
  718. * file for that resource on disk
  719. * @param {array} branch
  720. * path parts for branch to chop off file path.
  721. * @return {array}
  722. * array of path parts
  723. */
  724. function findProjectPath(file, branch) {
  725. let path = OS.Path.split(file.path).components;
  726. for (let i = 2; i <= branch.length; i++) {
  727. // work backwards until we find a differing directory name
  728. if (path[path.length - i] != branch[branch.length - i]) {
  729. return path.slice(0, path.length - i + 1);
  730. }
  731. }
  732. // if we don't find a differing directory, just chop off the branch
  733. return path.slice(0, path.length - branch.length);
  734. }
  735. /**
  736. * Find the parts of a uri past the root it shares with another uri. e.g:
  737. * "http://localhost/built/a.scss" and "http://localhost/src/a.css"
  738. * would yield ["built", "a.scss"] and ["src", "a.css"]
  739. *
  740. * @param {nsIURI} origUri
  741. * uri to find unshared branch of. Usually is uri for original source.
  742. * @param {nsIURI} uri
  743. * uri to compare against to get a shared root
  744. * @return {object}
  745. * object with 'branch' and 'origBranch' array of path parts for branch
  746. */
  747. function findUnsharedBranches(origUri, uri) {
  748. origUri = OS.Path.split(origUri.path).components;
  749. uri = OS.Path.split(uri.path).components;
  750. for (let i = 0; i < uri.length - 1; i++) {
  751. if (uri[i] != origUri[i]) {
  752. return {
  753. branch: uri.slice(i),
  754. origBranch: origUri.slice(i)
  755. };
  756. }
  757. }
  758. return {
  759. branch: uri,
  760. origBranch: origUri
  761. };
  762. }
  763. /**
  764. * Remove the query string from a url.
  765. *
  766. * @param {string} href
  767. * Url to remove query string from
  768. * @return {string}
  769. * Url without query string
  770. */
  771. function removeQuery(href) {
  772. return href.replace(/\?.*/, "");
  773. }