/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensions/default/UrlCodeHints/main.js
815 строк
31 KB
ficristo
Move most inline jslint directives to config files
12 авг 2016, 10:22
12 авг 2016, 10:22
074382a
Код
Авторство
О чём код?
/* * 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. */ define(function (require, exports, module) { "use strict"; // Brackets modules var AppInit = brackets.getModule("utils/AppInit"), CodeHintManager = brackets.getModule("editor/CodeHintManager"), CSSUtils = brackets.getModule("language/CSSUtils"), FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), HTMLUtils = brackets.getModule("language/HTMLUtils"), PreferencesManager = brackets.getModule("preferences/PreferencesManager"), ProjectManager = brackets.getModule("project/ProjectManager"), StringUtils = brackets.getModule("utils/StringUtils"), PathUtils = brackets.getModule("thirdparty/path-utils/path-utils"), Strings = brackets.getModule("strings"), Data = require("text!data.json"), urlHints, data, htmlAttrs, styleModes = ["css", "text/x-less", "text/x-scss"]; PreferencesManager.definePreference("codehint.UrlCodeHints", "boolean", true, { description: Strings.DESCRIPTION_URL_CODE_HINTS }); /** * @constructor */ function UrlCodeHints() {} /** * Helper function to create a list of urls to existing files based on the query. * @param {{queryStr: string}} query -- a query object, used to filter the code hints * * @return {Array.<string>|$.Deferred} The (possibly deferred) hints. */ UrlCodeHints.prototype._getUrlList = function (query) { var directory, doc, docDir, queryDir = "", queryUrl, result = [], self, targetDir, unfiltered = []; doc = this.editor && this.editor.document; if (!doc || !doc.file) { return result; } docDir = FileUtils.getDirectoryPath(doc.file.fullPath); // get relative path from query string queryUrl = PathUtils.parseUrl(query.queryStr); if (queryUrl) { queryDir = queryUrl.directory; } // build target folder path if (queryDir.length > 0 && queryDir[0] === "/") { // site-root relative path targetDir = ProjectManager.getProjectRoot().fullPath + decodeURI(queryDir).substring(1); } else { // page relative path targetDir = docDir + decodeURI(queryDir); } // Get list of files from target folder. Getting the file/folder info is an // asynch operation, so it works like this: // // The initial pass initiates the asynchronous retrieval of data and returns an // empty list, so no code hints are displayed. In the async callback, the code // hints and the original query are stored in a cache, and then the process to // show code hints is re-initiated. // // During the next pass, there should now be code hints cached from the initial // pass, but user may have typed while file/folder info was being retrieved from // disk, so we need to make sure code hints still apply to current query. If so, // display them, otherwise, clear cache and start over. // // As user types within a folder, the same unfiltered file/folder list is still // valid and re-used from cache. Filtering based on user input is done outside // of this method. When user moves to a new folder, then the cache is deleted, // and file/folder info for new folder is then retrieved. if (this.cachedHints) { // url hints have been cached, so determine if they're stale if (!this.cachedHints.query || this.cachedHints.query.tag !== query.tag || this.cachedHints.query.attrName !== query.attrName || this.cachedHints.queryDir !== queryDir || this.cachedHints.docDir !== docDir) { // delete stale cache this.cachedHints = null; } } if (this.cachedHints) { // use cached hints unfiltered = this.cachedHints.unfiltered; } else { directory = FileSystem.getDirectoryForPath(targetDir); self = this; if (self.cachedHints && self.cachedHints.deferred) { self.cachedHints.deferred.reject(); } // create empty object so we can detect "waiting" state self.cachedHints = {}; self.cachedHints.deferred = $.Deferred(); self.cachedHints.unfiltered = []; directory.getContents(function (err, contents) { var currentDeferred, entryStr, syncResults; if (!err) { contents.forEach(function (entry) { if (ProjectManager.shouldShow(entry)) { // convert to doc relative path entryStr = queryDir + entry._name; if (entry._isDirectory) { entryStr += "/"; } // code hints show the unencoded string so the // choices are easier to read. The encoded string // will still be inserted into the editor. unfiltered.push(entryStr); } }); self.cachedHints.unfiltered = unfiltered; self.cachedHints.query = query; self.cachedHints.queryDir = queryDir; self.cachedHints.docDir = docDir; if (self.cachedHints.deferred.state() !== "rejected") { currentDeferred = self.cachedHints.deferred; // Since we've cached the results, the next call to _getUrlList should be synchronous. // If it isn't, we've got a problem and should reject both the current deferred // and any new deferred that got created on the call. syncResults = self._getUrlList(query); if (syncResults instanceof Array) { currentDeferred.resolveWith(self, [syncResults]); } else { if (currentDeferred && currentDeferred.state() === "pending") { currentDeferred.reject(); } if (self.cachedHints.deferred && self.cachedHints.deferred.state() === "pending") { self.cachedHints.deferred.reject(); self.cachedHints.deferred = null; } } } } }); return self.cachedHints.deferred; } // build list // without these entries, typing "../" will not display entries for containing folder if (queryUrl.filename === ".") { result.push(queryDir + "."); } else if (queryUrl.filename === "..") { result.push(queryDir + ".."); } // add file/folder entries unfiltered.forEach(function (item) { result.push(item); }); // TODO: filter by desired file type based on tag, type attr, etc. // TODO: add list item to top of list to popup modal File Finder dialog // New string: "Browse..." or "Choose a File..." // Command: Commands.FILE_OPEN return result; }; /** * Helper function that determines the possible value hints for a given html tag/attribute name pair * * @param {{queryStr: string}} query * The current query * * @return {{hints: (Array.<string>|$.Deferred), sortFunc: ?function(string, string): number}} * The (possibly deferred) hints and the sort function to use on thise hints. */ UrlCodeHints.prototype._getUrlHints = function (query) { var hints = [], sortFunc; // Do not show hints after "?" in url if (query.queryStr.indexOf("?") === -1) { // Default behavior for url hints is do not close on select. this.closeOnSelect = false; hints = this._getUrlList(query); sortFunc = StringUtils.urlSort; } return { hints: hints, sortFunc: sortFunc }; }; /** * Determines whether url hints are available in the current editor * context. * * @param {Editor} editor * A non-null editor object for the active window. * * @param {string} implicitChar * Either null, if the hinting request was explicit, or a single character * that represents the last insertion and that indicates an implicit * hinting request. * * @return {boolean} * Determines whether the current provider is able to provide hints for * the given editor context and, in case implicitChar is non-null, * whether it is appropriate to do so. */ UrlCodeHints.prototype.hasHints = function (editor, implicitChar) { var mode = editor.getModeForSelection(); if (mode === "html") { return this.hasHtmlHints(editor, implicitChar); } else if (styleModes.indexOf(mode) > -1) { return this.hasCssHints(editor, implicitChar); } return false; }; /** * Helper function for hasHints() for CSS. * * @param {Editor} editor * A non-null editor object for the active window. * * @param {string} implicitChar * Either null, if the hinting request was explicit, or a single character * that represents the last insertion and that indicates an implicit * hinting request. * * @return {boolean} * Determines whether the current provider is able to provide hints for * the given editor context and, in case implicitChar is non-null, * whether it is appropriate to do so. */ UrlCodeHints.prototype.hasCssHints = function (editor, implicitChar) { this.editor = editor; var cursor = this.editor.getCursorPos(); this.info = CSSUtils.getInfoAtPos(editor, cursor); if (this.info.context !== CSSUtils.PROP_VALUE && this.info.context !== CSSUtils.IMPORT_URL) { return false; } // collect existing value var i, val = ""; for (i = 0; i <= this.info.index && i < this.info.values.length; i++) { if (i < this.info.index) { val += this.info.values[i]; } else { val += this.info.values[i].substring(0, this.info.offset); } } // starts with "url(" ? if (val.match(/^\s*url\(/i)) { return true; } return false; }; /** * Helper function for hasHints() for HTML. * * @param {Editor} editor * A non-null editor object for the active window. * * @param {string} implicitChar * Either null, if the hinting request was explicit, or a single character * that represents the last insertion and that indicates an implicit * hinting request. * * @return {boolean} * Determines whether the current provider is able to provide hints for * the given editor context and, in case implicitChar is non-null, * whether it is appropriate to do so. */ UrlCodeHints.prototype.hasHtmlHints = function (editor, implicitChar) { var tagInfo, query, tokenType; this.editor = editor; tagInfo = HTMLUtils.getTagInfo(editor, editor.getCursorPos()); query = null; tokenType = tagInfo.position.tokenType; if (tokenType === HTMLUtils.ATTR_VALUE) { // Verify that attribute name has hintable values if (htmlAttrs[tagInfo.attr.name]) { if (tagInfo.position.offset >= 0) { query = tagInfo.attr.value.slice(0, tagInfo.position.offset); } else { // We get negative offset for a quoted attribute value with some leading whitespaces // as in <a rel= "rtl" where the cursor is just to the right of the "=". // So just set the queryStr to an empty string. query = ""; } var hintsAndSortFunc = this._getUrlHints({queryStr: query}), hints = hintsAndSortFunc.hints; if (hints instanceof Array) { // If we got synchronous hints, check if we have something we'll actually use var i, foundPrefix = false; query = query.toLowerCase(); for (i = 0; i < hints.length; i++) { if (hints[i].toLowerCase().indexOf(query) === 0) { foundPrefix = true; break; } } if (!foundPrefix) { query = null; } } } } return (query !== null); }; /** * Returns a list of available url hints, if possible, for the current * editor context. * * @return {jQuery.Deferred|{ * hints: Array.<string|jQueryObject>, * match: string, * selectInitial: boolean, * handleWideResults: boolean}} * Null if the provider wishes to end the hinting session. Otherwise, a * response object that provides * 1. a sorted array hints that consists of strings * 2. a string match that is used by the manager to emphasize matching * substrings when rendering the hint list * 3. a boolean that indicates whether the first result, if one exists, should be * selected by default in the hint list window. * 4. handleWideResults, a boolean (or undefined) that indicates whether * to allow result string to stretch width of display. */ UrlCodeHints.prototype.getHints = function (key) { var mode = this.editor.getModeForSelection(), cursor = this.editor.getCursorPos(), filter = "", hints = [], sortFunc, query = { queryStr: "" }, result = []; if (mode === "html") { var tagInfo = HTMLUtils.getTagInfo(this.editor, cursor), tokenType = tagInfo.position.tokenType; if (tokenType !== HTMLUtils.ATTR_VALUE || !htmlAttrs[tagInfo.attr.name]) { return null; } if (tagInfo.position.offset >= 0) { query.queryStr = tagInfo.attr.value.slice(0, tagInfo.position.offset); } this.info = tagInfo; } else if (styleModes.indexOf(mode) > -1) { this.info = CSSUtils.getInfoAtPos(this.editor, cursor); var context = this.info.context; if (context !== CSSUtils.PROP_VALUE && context !== CSSUtils.IMPORT_URL) { return null; } // Cursor is in an existing property value or partially typed value if (this.info.index !== -1) { // Collect value up to (item) index/(char) offset var i, val = ""; for (i = 0; i < this.info.index; i++) { val += this.info.values[i]; } // index may exceed length of array for multiple-value case if (this.info.index < this.info.values.length) { val += this.info.values[this.info.index].substr(0, this.info.offset); } // Strip "url(" val = val.replace(/^\s*url\(/i, ""); // Keep track of leading whitespace and strip it var matchWhitespace = val.match(/^\s*/); if (matchWhitespace) { this.info.leadingWhitespace = matchWhitespace[0]; val = val.substring(matchWhitespace[0].length); } else { this.info.leadingWhitespace = null; } // Keep track of opening quote and strip it if (val.match(/^["']/)) { this.info.openingQuote = val[0]; val = val.substring(1); } else { this.info.openingQuote = null; } query.queryStr = val; } } else { return null; } if (query.queryStr !== null) { filter = query.queryStr; var hintsAndSortFunc = this._getUrlHints(query); hints = hintsAndSortFunc.hints; sortFunc = hintsAndSortFunc.sortFunc; } this.info.filter = filter; if (hints instanceof Array && hints.length) { // Array was returned var lowerCaseFilter = filter.toLowerCase(); console.assert(!result.length); result = $.map(hints, function (item) { if (item.toLowerCase().indexOf(lowerCaseFilter) === 0) { return item; } }).sort(sortFunc); return { hints: result, match: query.queryStr, selectInitial: true, handleWideResults: false }; } else if (hints instanceof Object && hints.hasOwnProperty("done")) { // Deferred hints were returned var deferred = $.Deferred(); hints.done(function (asyncHints) { var lowerCaseFilter = filter.toLowerCase(); result = $.map(asyncHints, function (item) { if (item.toLowerCase().indexOf(lowerCaseFilter) === 0) { return item; } }).sort(sortFunc); deferred.resolveWith(this, [{ hints: result, match: query.queryStr, selectInitial: true, handleWideResults: false }]); }); return deferred; } return null; }; /** * Inserts a given url hint into the current editor context. * * @param {jQuery.Object} completion * The hint to be inserted into the editor context. * * @return {boolean} * Indicates whether the manager should follow hint insertion with an * additional explicit hint request. */ UrlCodeHints.prototype.insertHint = function (completion) { var mode = this.editor.getModeForSelection(); // Encode the string just prior to inserting the hint into the editor completion = encodeURI(completion); if (mode === "html") { return this.insertHtmlHint(completion); } else if (styleModes.indexOf(mode) > -1) { return this.insertCssHint(completion); } return false; }; /** * Get distance between 2 positions. * * Assumption: pos2 >= pos1 * * Note that this function is designed to work on CSSUtils info.values array, * so this could be made a method if that is converted to an object. * * @param {Array.<string>} array - strings to be searched * @param {{index: number, offset: number}} pos1 - starting index/offset in index string * @param {{index: number, offset: number}} pos2 - ending index/offset in index string * * @return {number} * Number of characters between 2 positions */ UrlCodeHints.prototype.getCharOffset = function (array, pos1, pos2) { var i, count = 0; if (pos1.index === pos2.index) { return (pos2.offset >= pos1.offset) ? (pos2.offset - pos1.offset) : 0; } else if (pos1.index < pos2.index) { if (pos1.index < 0 || pos1.index >= array.length || pos2.index < 0 || pos2.index >= array.length) { return 0; } for (i = pos1.index; i <= pos2.index; i++) { if (i === pos1.index) { count += (array[i].length - pos1.offset); } else if (i === pos2.index) { count += pos2.offset; } else { count += array[i].length; } } } return count; }; /** * Finds next position in array of specified char. * * Note that this function is designed to work on CSSUtils info.values array, * so this could be made a method if that is converted to an object. * * @param {Array} array - strings to be searched * @param {string} ch - char to search for * @param {{index: number, offset: number}} pos - starting index/offset in index string * * @return {{index: number, offset: number}} * Index of array, and offset in string where char found. */ UrlCodeHints.prototype.findNextPosInArray = function (array, ch, pos) { var i, o, searchOffset; for (i = pos.index; i < array.length; i++) { // Only use offset on index, then offset of 0 after that searchOffset = (i === pos.index) ? pos.offset : 0; o = array[i].indexOf(ch, searchOffset); if (o !== -1) { return { index: i, offset: o }; } } return { index: -1, offset: -1 }; }; /** * Inserts a given css url hint into the current editor context. * * @param {jQuery.Object} completion * The hint to be inserted into the editor context. * * @return {boolean} * Indicates whether the manager should follow hint insertion with an * additional explicit hint request. */ UrlCodeHints.prototype.insertCssHint = function (completion) { var cursor = this.editor.getCursorPos(), start = { line: cursor.line, ch: cursor.ch }, end = { line: cursor.line, ch: cursor.ch }; var hasClosingQuote = false, hasClosingParen = false, insertText = completion, moveLen = 0, closingPos = { index: -1, offset: -1 }, searchResult = { index: -1, offset: -1 }; if (this.info.context !== CSSUtils.PROP_VALUE && this.info.context !== CSSUtils.IMPORT_URL) { return false; } // Special handling for URL hinting -- if the completion is a file name // and not a folder, then close the code hint list. if (!this.closeOnSelect && completion.match(/\/$/) === null) { this.closeOnSelect = true; } // Look for optional closing quote if (this.info.openingQuote) { closingPos = this.findNextPosInArray(this.info.values, this.info.openingQuote, this.info); hasClosingQuote = (closingPos.index !== -1); } // Look for closing paren if (hasClosingQuote) { searchResult = this.findNextPosInArray(this.info.values, ")", closingPos); hasClosingParen = (searchResult.index !== -1); } else { // index may exceed length of array for multiple-value case closingPos = this.findNextPosInArray(this.info.values, ")", this.info); hasClosingParen = (closingPos.index !== -1); } // Insert folder names, but replace file names, so if a file is selected // (i.e. closeOnSelect === true), then adjust insert char positions to // replace existing value, if there is a closing paren if (this.closeOnSelect) { if (closingPos.index !== -1) { end.ch += this.getCharOffset(this.info.values, this.info, closingPos); } } else { // If next char is "/", then overwrite it since we're inserting a "/" var nextSlash = this.findNextPosInArray(this.info.values, "/", this.info); if (nextSlash.index === this.info.index && nextSlash.offset === this.info.offset) { end.ch += 1; } } if (this.info.filter.length > 0) { start.ch -= this.info.filter.length; } // Append matching quote, whitespace, paren if (this.info.openingQuote && !hasClosingQuote) { insertText += this.info.openingQuote; } if (!hasClosingParen) { // Add trailing whitespace to match leading whitespace if (this.info.leadingWhitespace) { insertText += this.info.leadingWhitespace; } insertText += ")"; } // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance // directly to replace the range instead of using the Document, as we should. The // reason is due to a flaw in our current document synchronization architecture when // inline editors are open. this.editor._codeMirror.replaceRange(insertText, start, end); // Adjust cursor position if (this.closeOnSelect) { // If there is existing closing quote and/or paren, move the cursor past them moveLen = (hasClosingQuote ? 1 : 0) + (hasClosingParen ? 1 : 0); if (moveLen > 0) { this.editor.setCursorPos(start.line, start.ch + completion.length + moveLen); } return false; } else { // If closing quote and/or paren are added, move the cursor to where it would have been moveLen = ((this.info.openingQuote && !hasClosingQuote) ? 1 : 0) + (!hasClosingParen ? 1 : 0); if (moveLen > 0) { this.editor.setCursorPos(start.line, start.ch + completion.length); } } return true; }; /** * Inserts a given html url hint into the current editor context. * * @param {jQuery.Object} completion * The hint to be inserted into the editor context. * * @return {boolean} * Indicates whether the manager should follow hint insertion with an * additional explicit hint request. */ UrlCodeHints.prototype.insertHtmlHint = function (completion) { var cursor = this.editor.getCursorPos(), start = {line: -1, ch: -1}, end = {line: -1, ch: -1}, tagInfo = HTMLUtils.getTagInfo(this.editor, cursor), tokenType = tagInfo.position.tokenType, charCount = 0, endQuote = "", shouldReplace = false; if (tokenType === HTMLUtils.ATTR_VALUE) { // Special handling for URL hinting -- if the completion is a file name // and not a folder, then close the code hint list. if (!this.closeOnSelect && completion.match(/\/$/) === null) { this.closeOnSelect = true; // Insert folder names, but replace file names shouldReplace = true; } if (!tagInfo.attr.hasEndQuote) { endQuote = tagInfo.attr.quoteChar; if (endQuote) { completion += endQuote; } else if (tagInfo.position.offset === 0) { completion = "\"" + completion + "\""; } } if (shouldReplace) { // Replace entire value charCount = tagInfo.attr.value.length; } else { // Replace filter (to insert new selection) charCount = this.info.filter.length; // If next char is "/", then overwrite it since we're inserting a "/" if (this.info.attr.value.length > charCount && this.info.attr.value[charCount] === "/") { charCount += 1; } } } end.line = start.line = cursor.line; start.ch = cursor.ch - tagInfo.position.offset; end.ch = start.ch + charCount; this.editor.document.replaceRange(completion, start, end); if (!this.closeOnSelect) { // If we append the missing quote, then we need to adjust the cursor postion // to keep the code hint list open. if (tokenType === HTMLUtils.ATTR_VALUE && !tagInfo.attr.hasEndQuote) { this.editor.setCursorPos(start.line, start.ch + completion.length - 1); } return true; } if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.hasEndQuote) { // Move the cursor to the right of the existing end quote after value insertion. this.editor.setCursorPos(start.line, start.ch + completion.length + 1); } return false; }; function _clearCachedHints() { // Verify cache exists and is not deferred if (urlHints && urlHints.cachedHints && urlHints.cachedHints.deferred && urlHints.cachedHints.deferred.state() !== "pending") { // Cache may or may not be stale. Main benefit of cache is to limit async lookups // during typing. File tree updates cannot happen during typing, so it's probably // not worth determining whether cache may still be valid. Just delete it. urlHints.cachedHints = null; } } AppInit.appReady(function () { data = JSON.parse(Data); htmlAttrs = data.htmlAttrs; urlHints = new UrlCodeHints(); CodeHintManager.registerHintProvider(urlHints, ["css", "html", "less", "scss"], 5); FileSystem.on("change", _clearCachedHints); FileSystem.on("rename", _clearCachedHints); // For unit testing exports.hintProvider = urlHints; }); });