adobe/brackets

View on GitHub
src/editor/EditorStatusBar.js

Summary

Maintainability
C
1 day
Test Coverage
/*
 * Copyright (c) 2013 - 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.
 *
 */

/**
 * Manages parts of the status bar related to the current editor's state.
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var _                    = require("thirdparty/lodash"),
        AnimationUtils       = require("utils/AnimationUtils"),
        AppInit              = require("utils/AppInit"),
        DropdownButton       = require("widgets/DropdownButton").DropdownButton,
        EditorManager        = require("editor/EditorManager"),
        MainViewManager      = require("view/MainViewManager"),
        Editor               = require("editor/Editor").Editor,
        KeyEvent             = require("utils/KeyEvent"),
        LanguageManager      = require("language/LanguageManager"),
        PreferencesManager   = require("preferences/PreferencesManager"),
        StatusBar            = require("widgets/StatusBar"),
        Strings              = require("strings"),
        FileUtils            = require("file/FileUtils"),
        InMemoryFile         = require("document/InMemoryFile"),
        Dialogs              = require("widgets/Dialogs"),
        DefaultDialogs       = require("widgets/DefaultDialogs"),
        ProjectManager       = require("project/ProjectManager"),
        Async                = require("utils/Async"),
        FileSystem           = require("filesystem/FileSystem"),
        CommandManager       = require("command/CommandManager"),
        Commands             = require("command/Commands"),
        DocumentManager      = require("document/DocumentManager"),
        StringUtils          = require("utils/StringUtils"),
        HealthLogger         = require("utils/HealthLogger");
    
    var SupportedEncodingsText = require("text!supported-encodings.json"),
        SupportedEncodings = JSON.parse(SupportedEncodingsText);

    /* StatusBar indicators */
    var languageSelect, // this is a DropdownButton instance
        encodingSelect, // this is a DropdownButton instance
        $cursorInfo,
        $fileInfo,
        $indentType,
        $indentWidthLabel,
        $indentWidthInput,
        $statusOverwrite;

    /** Special list item for the 'set as default' gesture in language switcher dropdown */
    var LANGUAGE_SET_AS_DEFAULT = {};


    /**
     * Determine string based on count
     * @param {number} number Count
     * @param {string} singularStr Singular string
     * @param {string} pluralStr Plural string
     * @return {string} Proper string to use for count
     */
    function _formatCountable(number, singularStr, pluralStr) {
        return StringUtils.format(number > 1 ? pluralStr : singularStr, number);
    }

    /**
     * Update file mode
     * @param {Editor} editor Current editor
     */
    function _updateLanguageInfo(editor) {
        var doc = editor.document,
            lang = doc.getLanguage();

        // Show the current language as button title
        languageSelect.$button.text(lang.getName());
    }

    /**
     * Update encoding
     * @param {Editor} editor Current editor
     */
    function _updateEncodingInfo(editor) {
        var doc = editor.document;

        // Show the current encoding as button title
        if (!doc.file._encoding) {
            doc.file._encoding = "UTF-8";
        }
        encodingSelect.$button.text(doc.file._encoding);
    }

    /**
     * Update file information
     * @param {Editor} editor Current editor
     */
    function _updateFileInfo(editor) {
        var lines = editor.lineCount();
        $fileInfo.text(_formatCountable(lines, Strings.STATUSBAR_LINE_COUNT_SINGULAR, Strings.STATUSBAR_LINE_COUNT_PLURAL));
    }

    /**
     * Update indent type and size
     * @param {string} fullPath Path to file in current editor
     */
    function _updateIndentType(fullPath) {
        var indentWithTabs = Editor.getUseTabChar(fullPath);
        $indentType.text(indentWithTabs ? Strings.STATUSBAR_TAB_SIZE : Strings.STATUSBAR_SPACES);
        $indentType.attr("title", indentWithTabs ? Strings.STATUSBAR_INDENT_TOOLTIP_SPACES : Strings.STATUSBAR_INDENT_TOOLTIP_TABS);
        $indentWidthLabel.attr("title", indentWithTabs ? Strings.STATUSBAR_INDENT_SIZE_TOOLTIP_TABS : Strings.STATUSBAR_INDENT_SIZE_TOOLTIP_SPACES);
    }

    /**
     * Get indent size based on type
     * @param {string} fullPath Path to file in current editor
     * @return {number} Indent size
     */
    function _getIndentSize(fullPath) {
        return Editor.getUseTabChar(fullPath) ? Editor.getTabSize(fullPath) : Editor.getSpaceUnits(fullPath);
    }

    /**
     * Update indent size
     * @param {string} fullPath Path to file in current editor
     */
    function _updateIndentSize(fullPath) {
        var size = _getIndentSize(fullPath);
        $indentWidthLabel.text(size);
        $indentWidthInput.val(size);
    }

    /**
     * Toggle indent type
     */
    function _toggleIndentType() {
        var current = EditorManager.getActiveEditor(),
            fullPath = current && current.document.file.fullPath;

        Editor.setUseTabChar(!Editor.getUseTabChar(fullPath), fullPath);
        _updateIndentType(fullPath);
        _updateIndentSize(fullPath);
    }

    /**
     * Update cursor(s)/selection(s) information
     * @param {Event} event (unused)
     * @param {Editor} editor Current editor
     */
    function _updateCursorInfo(event, editor) {
        editor = editor || EditorManager.getActiveEditor();

        // compute columns, account for tab size
        var cursor = editor.getCursorPos(true);

        var cursorStr = StringUtils.format(Strings.STATUSBAR_CURSOR_POSITION, cursor.line + 1, cursor.ch + 1);

        var sels = editor.getSelections(),
            selStr = "";

        if (sels.length > 1) {
            //Send analytics data for multicursor use
            HealthLogger.sendAnalyticsData(
                "multiCursor",
                "usage",
                "multiCursor",
                "use"
            );
            selStr = StringUtils.format(Strings.STATUSBAR_SELECTION_MULTIPLE, sels.length);
        } else if (editor.hasSelection()) {
            var sel = sels[0];
            if (sel.start.line !== sel.end.line) {
                var lines = sel.end.line - sel.start.line + 1;
                if (sel.end.ch === 0) {
                    lines--;  // end line is exclusive if ch is 0, inclusive otherwise
                }
                selStr = _formatCountable(lines, Strings.STATUSBAR_SELECTION_LINE_SINGULAR, Strings.STATUSBAR_SELECTION_LINE_PLURAL);
            } else {
                var cols = editor.getColOffset(sel.end) - editor.getColOffset(sel.start);  // end ch is exclusive always
                selStr = _formatCountable(cols, Strings.STATUSBAR_SELECTION_CH_SINGULAR, Strings.STATUSBAR_SELECTION_CH_PLURAL);
            }
        }
        $cursorInfo.text(cursorStr + selStr);
    }

    /**
     * Change indent size
     * @param {string} fullPath Path to file in current editor
     * @param {string} value Size entered into status bar
     */
    function _changeIndentWidth(fullPath, value) {
        $indentWidthLabel.removeClass("hidden");
        $indentWidthInput.addClass("hidden");

        // remove all event handlers from the input field
        $indentWidthInput.off("blur keyup");

        // restore focus to the editor
        MainViewManager.focusActivePane();

        var valInt = parseInt(value, 10);
        if (Editor.getUseTabChar(fullPath)) {
            if (!Editor.setTabSize(valInt, fullPath)) {
                return;     // validation failed
            }
        } else {
            if (!Editor.setSpaceUnits(valInt, fullPath)) {
                return;     // validation failed
            }
        }

        // update indicator
        _updateIndentSize(fullPath);

        // column position may change when tab size changes
        _updateCursorInfo();
    }

    /**
     * Update insert/overwrite label
     * @param {Event} event (unused)
     * @param {Editor} editor Current editor
     * @param {string} newstate New overwrite state
     * @param {boolean=} doNotAnimate True if state should not be animated
     */
    function _updateOverwriteLabel(event, editor, newstate, doNotAnimate) {
        if ($statusOverwrite.text() === (newstate ? Strings.STATUSBAR_OVERWRITE : Strings.STATUSBAR_INSERT)) {
            // label already up-to-date
            return;
        }

        $statusOverwrite.text(newstate ? Strings.STATUSBAR_OVERWRITE : Strings.STATUSBAR_INSERT);

        if (!doNotAnimate) {
            AnimationUtils.animateUsingClass($statusOverwrite[0], "flash", 1500);
        }
    }

    /**
     * Update insert/overwrite indicator
     * @param {Event} event (unused)
     */
    function _updateEditorOverwriteMode(event) {
        var editor = EditorManager.getActiveEditor(),
            newstate = !editor._codeMirror.state.overwrite;

        // update label with no transition
        _updateOverwriteLabel(event, editor, newstate, true);
        editor.toggleOverwrite(newstate);
    }

    /**
     * Initialize insert/overwrite indicator
     * @param {Editor} currentEditor Current editor
     */
    function _initOverwriteMode(currentEditor) {
        currentEditor.toggleOverwrite($statusOverwrite.text() === Strings.STATUSBAR_OVERWRITE);
        $statusOverwrite.attr("title", Strings.STATUSBAR_INSOVR_TOOLTIP);
    }

    /**
     * Handle active editor change event
     * @param {Event} event (unused)
     * @param {Editor} current Current editor
     * @param {Editor} previous Previous editor
     */
    function _onActiveEditorChange(event, current, previous) {
        if (previous) {
            previous.off(".statusbar");
            previous.document.off(".statusbar");
            previous.document.releaseRef();
        }

        if (!current) {
            StatusBar.hideAllPanes();
        } else {
            var fullPath = current.document.file.fullPath;
            StatusBar.showAllPanes();

            current.on("cursorActivity.statusbar", _updateCursorInfo);
            current.on("optionChange.statusbar", function () {
                _updateIndentType(fullPath);
                _updateIndentSize(fullPath);
            });
            current.on("change.statusbar", function () {
                // async update to keep typing speed smooth
                window.setTimeout(function () { _updateFileInfo(current); }, 0);
            });
            current.on("overwriteToggle.statusbar", _updateOverwriteLabel);

            current.document.addRef();
            current.document.on("languageChanged.statusbar", function () {
                _updateLanguageInfo(current);
            });

            _updateCursorInfo(null, current);
            _updateLanguageInfo(current);
            _updateEncodingInfo(current);
            _updateFileInfo(current);
            _initOverwriteMode(current);
            _updateIndentType(fullPath);
            _updateIndentSize(fullPath);
        }
    }

    /**
     * Populate the languageSelect DropdownButton's menu with all registered Languages
     */
    function _populateLanguageDropdown() {
        // Get all non-binary languages
        var languages = _.values(LanguageManager.getLanguages()).filter(function (language) {
            return !language.isBinary();
        });

        // sort dropdown alphabetically
        languages.sort(function (a, b) {
            return a.getName().toLowerCase().localeCompare(b.getName().toLowerCase());
        });

        languageSelect.items = languages;

        // Add option to top of menu for persisting the override
        languageSelect.items.unshift("---");
        languageSelect.items.unshift(LANGUAGE_SET_AS_DEFAULT);
    }

    /**
     * Change the encoding and reload the current document.
     * If passed then save the preferred encoding in state.
     */
    function _changeEncodingAndReloadDoc(document) {
        var promise = document.reload();
        promise.done(function (text, readTimestamp) {
            encodingSelect.$button.text(document.file._encoding);
            // Store the preferred encoding in the state
            var projectRoot = ProjectManager.getProjectRoot(),
                context = {
                    location : {
                        scope: "user",
                        layer: "project",
                        layerID: projectRoot.fullPath
                    }
                };
            var encoding = PreferencesManager.getViewState("encoding", context);
            encoding[document.file.fullPath] = document.file._encoding;
            PreferencesManager.setViewState("encoding", encoding, context);
        });
        promise.fail(function (error) {
            console.log("Error reloading contents of " + document.file.fullPath, error);
        });
    }


    /**
     * Populate the encodingSelect DropdownButton's menu with all registered encodings
     */
    function _populateEncodingDropdown() {
        encodingSelect.items = SupportedEncodings;
    }

    /**
     * Initialize
     */
    function _init() {

        $cursorInfo         = $("#status-cursor");
        $fileInfo           = $("#status-file");
        $indentType         = $("#indent-type");
        $indentWidthLabel   = $("#indent-width-label");
        $indentWidthInput   = $("#indent-width-input");
        $statusOverwrite    = $("#status-overwrite");

        languageSelect      = new DropdownButton("", [], function (item, index) {
            var document = EditorManager.getActiveEditor().document,
                defaultLang = LanguageManager.getLanguageForPath(document.file.fullPath, true);

            if (item === LANGUAGE_SET_AS_DEFAULT) {
                var label = _.escape(StringUtils.format(Strings.STATUSBAR_SET_DEFAULT_LANG, LanguageManager.getCompoundFileExtension(document.file.fullPath)));
                return { html: label, enabled: document.getLanguage() !== defaultLang };
            }

            var html = _.escape(item.getName());

            // Show indicators for currently selected & default languages for the current file
            if (item === defaultLang) {
                html += " <span class='default-language'>" + Strings.STATUSBAR_DEFAULT_LANG + "</span>";
            }
            if (item === document.getLanguage()) {
                html = "<span class='checked-language'></span>" + html;
            }
            return html;
        });

        languageSelect.dropdownExtraClasses = "dropdown-status-bar";
        languageSelect.$button.addClass("btn-status-bar");
        $("#status-language").append(languageSelect.$button);
        languageSelect.$button.attr("title", Strings.STATUSBAR_LANG_TOOLTIP);


        encodingSelect = new DropdownButton("", [], function (item, index) {
            var document = EditorManager.getActiveEditor().document;
            var html = _.escape(item);

            // Show indicators for currently selected & default languages for the current file
            if (item === "UTF-8") {
                html += " <span class='default-language'>" + Strings.STATUSBAR_DEFAULT_LANG + "</span>";
            }
            if (item === document.file._encoding) {
                html = "<span class='checked-language'></span>" + html;
            }
            return html;
        });

        encodingSelect.dropdownExtraClasses = "dropdown-status-bar";
        encodingSelect.$button.addClass("btn-status-bar");
        $("#status-encoding").append(encodingSelect.$button);
        encodingSelect.$button.attr("title", Strings.STATUSBAR_ENCODING_TOOLTIP);


        // indentation event handlers
        $indentType.on("click", _toggleIndentType);
        $indentWidthLabel
            .on("click", function () {
                // update the input value before displaying
                var fullPath = EditorManager.getActiveEditor().document.file.fullPath;
                $indentWidthInput.val(_getIndentSize(fullPath));

                $indentWidthLabel.addClass("hidden");
                $indentWidthInput.removeClass("hidden");
                $indentWidthInput.focus();

                $indentWidthInput
                    .on("blur", function () {
                        _changeIndentWidth(fullPath, $indentWidthInput.val());
                    })
                    .on("keyup", function (event) {
                        if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
                            $indentWidthInput.blur();
                        } else if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                            _changeIndentWidth(fullPath, false);
                        }
                    });
            });

        $indentWidthInput.focus(function () { $indentWidthInput.select(); });

        // Language select change handler
        languageSelect.on("select", function (e, lang) {
            var document = EditorManager.getActiveEditor().document,
                fullPath = document.file.fullPath;

            var fileType = (document.file instanceof InMemoryFile) ? "newFile" : "existingFile",
                filelanguageName = lang ? lang._name : "";

            HealthLogger.sendAnalyticsData(
                HealthLogger.commonStrings.USAGE + HealthLogger.commonStrings.LANGUAGE_CHANGE
                + filelanguageName + fileType,
                HealthLogger.commonStrings.USAGE,
                HealthLogger.commonStrings.LANGUAGE_CHANGE,
                filelanguageName.toLowerCase(),
                fileType
            );

            if (lang === LANGUAGE_SET_AS_DEFAULT) {
                // Set file's current language in preferences as a file extension override (only enabled if not default already)
                var fileExtensionMap = PreferencesManager.get("language.fileExtensions");
                fileExtensionMap[LanguageManager.getCompoundFileExtension(fullPath)] = document.getLanguage().getId();
                PreferencesManager.set("language.fileExtensions", fileExtensionMap);

            } else {
                // Set selected language as a path override for just this one file (not persisted)
                var defaultLang = LanguageManager.getLanguageForPath(fullPath, true);
                // if default language selected, pass null to clear the override
                LanguageManager.setLanguageOverrideForPath(fullPath, lang === defaultLang ? null : lang);
            }
        });

        // Encoding select change handler
        encodingSelect.on("select", function (e, encoding) {
            var document = EditorManager.getActiveEditor().document,
                originalPath = document.file.fullPath,
                originalEncoding = document.file._encoding;

            document.file._encoding = encoding;
            if (!(document.file instanceof InMemoryFile) && document.isDirty) {
                CommandManager.execute(Commands.FILE_SAVE_AS, {doc: document}).done(function () {
                    var doc = DocumentManager.getCurrentDocument();
                    if (originalPath === doc.file.fullPath) {
                        _changeEncodingAndReloadDoc(doc);
                    } else {
                        document.file._encoding = originalEncoding;
                    }
                }).fail(function () {
                    document.file._encoding = originalEncoding;
                });
            } else if (document.file instanceof InMemoryFile) {
                encodingSelect.$button.text(encoding);
            } else if (!document.isDirty) {
                _changeEncodingAndReloadDoc(document);
            }
        });

        $statusOverwrite.on("click", _updateEditorOverwriteMode);
    }

    // Initialize: status bar focused listener
    EditorManager.on("activeEditorChange", _onActiveEditorChange);

    function _checkFileExistance(filePath, index, encoding) {
        var deferred = new $.Deferred(),
            fileEntry = FileSystem.getFileForPath(filePath);
    
        fileEntry.exists(function (err, exists) {
            if (!err && exists) {
                deferred.resolve();
            } else {
                delete encoding[filePath];
                deferred.reject();
            }
        });

        return deferred.promise();
    }

    ProjectManager.on("projectOpen", function () {
        var projectRoot = ProjectManager.getProjectRoot(),
            context = {
                location : {
                    scope: "user",
                    layer: "project",
                    layerID: projectRoot.fullPath
                }
            };
        var encoding = PreferencesManager.getViewState("encoding", context);
        if (!encoding) {
            encoding = {};
            PreferencesManager.setViewState("encoding", encoding, context);
        }
        Async.doSequentially(Object.keys(encoding), function (filePath, index) {
            return _checkFileExistance(filePath, index, encoding);
        }, false)
            .always(function () {
                PreferencesManager.setViewState("encoding", encoding, context);
            });
    });

    AppInit.htmlReady(_init);
    AppInit.appReady(function () {
        // Populate language switcher with all languages after startup; update it later if this set changes
        _populateLanguageDropdown();
        _populateEncodingDropdown();
        LanguageManager.on("languageAdded languageModified", _populateLanguageDropdown);
        _onActiveEditorChange(null, EditorManager.getActiveEditor(), null);
        StatusBar.show();
    });
});