diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 33d0aa9286f..348badcbca3 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -40,7 +40,8 @@ define(function (require, exports, module) { EditorManager = require("editor/EditorManager"), HTMLUtils = require("language/HTMLUtils"), ProjectManager = require("project/ProjectManager"), - TokenUtils = require("utils/TokenUtils"); + TokenUtils = require("utils/TokenUtils"), + _ = require("thirdparty/lodash"); // Constants var SELECTOR = "selector", @@ -128,20 +129,25 @@ define(function (require, exports, module) { * @param {Array.=} values An array of property values * @param {boolean=} isNewItem If this is true, then the value in index refers to the index at which a new item * is going to be inserted and should not be used for accessing an existing value in values array. + * @param {{start: {line: number, ch: number}, + * end: {line: number, ch: number}}=} range A range object with a start position and an end position * @return {{context: string, * offset: number, * name: string, * index: number, * values: Array., - * isNewItem: boolean}} A CSS context info object. + * isNewItem: boolean, + * range: {start: {line: number, ch: number}, + * end: {line: number, ch: number}}}} A CSS context info object. */ - function createInfo(context, offset, name, index, values, isNewItem) { + function createInfo(context, offset, name, index, values, isNewItem, range) { var ruleInfo = { context: context || "", offset: offset || 0, name: name || "", index: -1, values: [], - isNewItem: (isNewItem) ? true : false }; + isNewItem: (isNewItem === true), + range: range }; if (context === PROP_VALUE || context === SELECTOR || context === IMPORT_URL) { ruleInfo.index = index; @@ -165,7 +171,9 @@ define(function (require, exports, module) { * name: string, * index: number, * values: Array., - * isNewItem: boolean}} A CSS context info object. + * isNewItem: boolean, + * range: {start: {line: number, ch: number}, + * end: {line: number, ch: number}}}} A CSS context info object. */ function _getPropNameInfo(ctx) { var propName = "", @@ -335,6 +343,43 @@ define(function (require, exports, module) { return propValues; } + /** + * @private + * Return a range object with a start position and an end position after + * skipping any whitespaces and all separators used before and after a + * valid property value. + * + * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} startCtx context + * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} endCtx context + * @return {{start: {line: number, ch: number}, + * end: {line: number, ch: number}}} A range object. + */ + function _getRangeForPropValue(startCtx, endCtx) { + var range = { "start": {}, + "end": {} }; + + // Skip the ":" and any leading whitespace + while (TokenUtils.moveNextToken(startCtx)) { + if (startCtx.token.string.trim()) { + break; + } + } + + // Skip the trailing whitespace and property separators. + while (endCtx.token.string === ";" || endCtx.token.string === "}" || + !endCtx.token.string.trim()) { + TokenUtils.movePrevToken(endCtx); + } + + range.start = _.clone(startCtx.pos); + range.start.ch = startCtx.token.start; + + range.end = _.clone(endCtx.pos); + range.end.ch = endCtx.token.end; + + return range; + } + /** * @private * Returns a context info object for the current CSS style rule @@ -345,7 +390,9 @@ define(function (require, exports, module) { * name: string, * index: number, * values: Array., - * isNewItem: boolean}} A CSS context info object. + * isNewItem: boolean, + * range: {start: {line: number, ch: number}, + * end: {line: number, ch: number}}}} A CSS context info object. */ function _getRuleInfoStartingFromPropValue(ctx, editor) { var propNamePos = $.extend({}, ctx.pos), @@ -361,7 +408,8 @@ define(function (require, exports, module) { canAddNewOne = false, testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line}, testToken = editor._codeMirror.getTokenAt(testPos, true), - propName; + propName, + range; // Get property name first. If we don't have a valid property name, then // return a default rule info. @@ -413,13 +461,21 @@ define(function (require, exports, module) { forwardCtx = TokenUtils.getInitialContext(editor._codeMirror, forwardPos); propValues = propValues.concat(_getSucceedingPropValues(forwardCtx, lastValue)); + if (propValues.length) { + range = _getRangeForPropValue(backwardCtx, forwardCtx); + } else { + // No property value, so just return the cursor pos as range + range = { "start": _.clone(ctx.pos), + "end": _.clone(ctx.pos) }; + } + // If current index is more than the propValues size, then the cursor is // at the end of the existing property values and is ready for adding another one. if (index === propValues.length) { canAddNewOne = true; } - return createInfo(PROP_VALUE, offset, propName, index, propValues, canAddNewOne); + return createInfo(PROP_VALUE, offset, propName, index, propValues, canAddNewOne, range); } /** @@ -432,7 +488,9 @@ define(function (require, exports, module) { * name: string, * index: number, * values: Array., - * isNewItem: boolean}} A CSS context info object. + * isNewItem: boolean, + * range: {start: {line: number, ch: number}, + * end: {line: number, ch: number}}}} A CSS context info object. */ function _getImportUrlInfo(ctx, editor) { var propNamePos = $.extend({}, ctx.pos), @@ -501,7 +559,9 @@ define(function (require, exports, module) { * name: string, * index: number, * values: Array., - * isNewItem: boolean}} A CSS context info object. + * isNewItem: boolean, + * range: {start: {line: number, ch: number}, + * end: {line: number, ch: number}}}} A CSS context info object. */ function getInfoAtPos(editor, constPos) { // We're going to be changing pos a lot, but we don't want to mess up diff --git a/test/spec/CSSUtils-test-files/ranges.css b/test/spec/CSSUtils-test-files/ranges.css new file mode 100644 index 00000000000..4612a7717b1 --- /dev/null +++ b/test/spec/CSSUtils-test-files/ranges.css @@ -0,0 +1,55 @@ +/* */ + +.foo { + shape-inside: circle(0 + at + 0 0 + ); + + shape-inside: circle (0px + at + 0px + + 0px + ); + + shape-inside: polygon(0 0, + 100px + + 0, + 100px 100px + + + ); + + + shape-inside: +polygon( + + + nonzero, + 0 0, + 100px +0, + 100px + + 100px + ); + } + + + +@keyframes colorize { + 0% { + -webkit-filter: grayscale(100%); + } + 100% { + -webkit-filter: + + + grayscale + ( 0 % ) + +; + } +} diff --git a/test/spec/CSSUtils-test.js b/test/spec/CSSUtils-test.js index cfff17fe28c..032eb74a70a 100644 --- a/test/spec/CSSUtils-test.js +++ b/test/spec/CSSUtils-test.js @@ -48,6 +48,7 @@ define(function (require, exports, module) { var contextTestCss = require("text!spec/CSSUtils-test-files/contexts.css"), selectorPositionsTestCss = require("text!spec/CSSUtils-test-files/selector-positions.css"), + rangesTestCss = require("text!spec/CSSUtils-test-files/ranges.css"), simpleTestCss = require("text!spec/CSSUtils-test-files/simple.css"); /** @@ -1773,6 +1774,7 @@ define(function (require, exports, module) { expect(result.isNewItem).toBe(expected.isNewItem === undefined ? false : expected.isNewItem); expect(result.index).toBe(expected.index === undefined ? -1 : expected.index); expect(result.values).toEqual(expected.values === undefined ? [] : expected.values); + expect(result.range).toEqual(expected.range); } function checkInfoAtOffsets(first, last, expected) { @@ -1835,6 +1837,8 @@ define(function (require, exports, module) { it("should return PROP_VALUE with 'new value' flag set immediately after colon", function () { [9, 85].forEach(function (offset) { + var range = (offset === 9) ? {start: { line: 1, ch: 11 }, end: { line: 1, ch: 15 }} + : {start: { line: 25, ch: 20 }, end: { line: 25, ch: 24 }}; result = CSSUtils.getInfoAtPos(testEditor, contextTest.offsets[offset]); expect(result).toEqual({ context: CSSUtils.PROP_VALUE, @@ -1842,7 +1846,8 @@ define(function (require, exports, module) { name: "width", index: 0, values: ["100%"], - isNewItem: true + isNewItem: true, + range: range }); }); }); @@ -1853,14 +1858,16 @@ define(function (require, exports, module) { name: "width", index: 0, values: ["100%"], - isNewItem: false + isNewItem: false, + range: {start: { line: 1, ch: 11 }, end: { line: 1, ch: 15 }} }); checkInfoAtOffsets(86, 88, { context: CSSUtils.PROP_VALUE, name: "width", index: 0, values: ["100%"], - isNewItem: false + isNewItem: false, + range: {start: { line: 25, ch: 20 }, end: { line: 25, ch: 24 }} }); }); @@ -1869,7 +1876,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 0, - values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'], + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); it("should return PROP_VALUE with 'new value' flag set at end of double-quoted multi-value property", function () { @@ -1880,7 +1888,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 1, - values: ['"Helvetica Neue",', 'Arial, ', 'sans-serif'] // whitespace after cursor is deliberately lost + values: ['"Helvetica Neue",', 'Arial, ', 'sans-serif'], // whitespace after cursor is deliberately lost + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); it("should return PROP_VALUE with correct values at beginning/middle of second multi-value property", function () { @@ -1888,7 +1897,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 1, - values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'], + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); it("should return PROP_VALUE with 'new value' flag set at end of second multi-value property", function () { @@ -1899,7 +1909,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 2, - values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'] // whitespace after cursor is deliberately lost + values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'], // whitespace after cursor is deliberately lost + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); it("should return PROP_VALUE with correct values at beginning/middle/end of third multi-value property", function () { @@ -1908,7 +1919,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 2, - values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'], + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); @@ -1918,7 +1930,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 0, - values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'] + values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); it("should return PROP_VALUE with 'new value' flag set at end of double-quoted multi-value multi-line property", function () { @@ -1929,7 +1942,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 1, - values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'] // whitespace after cursor is deliberately lost + values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'], // whitespace after cursor is deliberately lost + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); it("should return PROP_VALUE with correct values at beginning/middle of second multi-value multi-line property", function () { @@ -1937,7 +1951,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 1, - values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); it("should return PROP_VALUE with 'new value' flag set at end of second multi-value multi-line property", function () { @@ -1948,7 +1963,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 2, - values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'] // whitespace after cursor is deliberately lost + values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'], // whitespace after cursor is deliberately lost + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); it("should return PROP_VALUE with correct values at beginning/middle/end of third multi-value multi-line property", function () { @@ -1957,7 +1973,8 @@ define(function (require, exports, module) { context: CSSUtils.PROP_VALUE, name: "font-family", index: 2, - values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); @@ -1969,7 +1986,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 0, - values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'] + values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); @@ -1982,7 +2000,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: i, - values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'] + values: ['"Helvetica Neue",', 'Arial,', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); } @@ -1994,7 +2013,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 2, - values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial,', 'sans-serif'], + range: {start: { line: 20, ch: 8 }, end: { line: 22, ch: 18 }} }); }); }); // multi-line cases @@ -2007,7 +2027,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 0, - values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'] + values: ['"Helvetica Neue", ', 'Arial, ', 'sans-serif'], + range: {start: { line: 15, ch: 17 }, end: { line: 15, ch: 52 }} }); }); it("should return PROP_VALUE with 'new value' flag and existing values at end of line after comma (possibly with whitespace)", function () { @@ -2019,7 +2040,8 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 1, - values: ["Arial,"] + values: ["Arial,"], + range: {start: { line: 28 + ((i - 46) * 3), ch: 17 }, end: { line: 28 + ((i - 46) * 3), ch: 23 }} }); } for (i = 48; i <= 49; i++) { @@ -2030,12 +2052,15 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 1, - values: ["Arial, "] + values: ["Arial, "], + range: {start: { line: 34 + ((i - 48) * 3), ch: 17 }, end: { line: 34 + ((i - 48) * 3), ch: 23 }} }); } }); it("should return PROP_VALUE with 'new value' flag at end of line when there are no existing values", function () { + var lineArray = [41, 44, 47, 50, 112], + columnArray = [10, 11, 10, 11, 10]; for (i = 70; i <= 74; i++) { result = CSSUtils.getInfoAtPos(testEditor, contextTest.offsets[i]); expect(result).toEqual({ @@ -2044,7 +2069,9 @@ define(function (require, exports, module) { offset: 0, isNewItem: true, index: 0, - values: [] + values: [], + range: {start: { line: lineArray[i - 70], ch: columnArray[i - 70] }, + end: { line: lineArray[i - 70], ch: columnArray[i - 70] }} }); } }); @@ -2059,7 +2086,8 @@ define(function (require, exports, module) { offset: 0, index: i, values: ["rgba(50, ", "100, ", "200, ", "0.3)"], - isNewItem: false + isNewItem: false, + range: {start: { line: 54, ch: 11 }, end: { line: 54, ch: 34 }} }); } }); @@ -2072,7 +2100,8 @@ define(function (require, exports, module) { offset: 1, index: 1, values: ["linear-gradient(to ", "right, ", "rgba(255,", "255,", "0), ", "#fff)"], - isNewItem: false + isNewItem: false, + range: {start: { line: 71, ch: 16 }, end: { line: 71, ch: 64 }} }); }); @@ -2084,7 +2113,8 @@ define(function (require, exports, module) { offset: 1, index: 1, values: ["polygon(", "0 ", "0)"], - isNewItem: false + isNewItem: false, + range: {start: { line: 58, ch: 18 }, end: { line: 58, ch: 30 }} }); }); @@ -2096,7 +2126,8 @@ define(function (require, exports, module) { offset: 1, index: 1, values: ["polygon(", "nonzero, ", "0 ", "0)"], - isNewItem: false + isNewItem: false, + range: {start: { line: 62, ch: 18 }, end: { line: 62, ch: 39 }} }); }); @@ -2133,7 +2164,8 @@ define(function (require, exports, module) { name: "font-family", index: 0, values: ["'Helvetica Neue', ", "Arial"], - isNewItem: false + isNewItem: false, + range: {start: { line: 75, ch: 17 }, end: { line: 75, ch: 40 }} }); }); it("should properly parse values with special characters", function () { @@ -2146,12 +2178,14 @@ define(function (require, exports, module) { name: "font-family", index: 0, values: [values[i]], - isNewItem: false + isNewItem: false, + range: {start: { line: 79 + i, ch: 17 }, end: { line: 79 + i, ch: 17 + values[i].length }} }); } }); }); + describe("invalid contexts", function () { @@ -2193,6 +2227,95 @@ define(function (require, exports, module) { }); }); + // These are tests related to Shapes editor requirements for determining the start/end range of a css property + describe("CSS Context Info Ranges", function () { + + // NOTE: check ranges for simple cases without whitespace is + describe("ranging for getInfoAtPos results with whitespace", function () { + var testEditor, + result; + + beforeEach(function () { + var mock = SpecRunnerUtils.createMockEditor(rangesTestCss, "css"); + testEditor = mock.editor; + }); + + afterEach(function () { + SpecRunnerUtils.destroyMockEditor(testEditor.document); + testEditor = null; + }); + + it("should return the correct range of a prop when cursor is on whitespace between function args", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 20, line: 4}); + expect(result.range.start).toEqual({ + ch: 18, + line: 3 + }); + expect(result.range.end).toEqual({ + ch: 5, + line: 6 + }); + }); + + it("should return the correct range of a prop when cursor is between characters in function args", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 26, line: 9}); + expect(result.range.start).toEqual({ + ch: 18, + line: 8 + }); + expect(result.range.end).toEqual({ + ch: 5, + line: 13 + }); + }); + it("should return the correct range of a prop when cursor is between characters in prop name with function args with whitespace", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 21, line: 15}); + expect(result.range.start).toEqual({ + ch: 18, + line: 15 + }); + expect(result.range.end).toEqual({ + ch: 5, + line: 22 + }); + }); + it("should return the correct range of a prop when cursor is on function arg delimiter with whitespace", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 29, line: 30}); + expect(result.range.start).toEqual({ + ch: 0, + line: 26 + }); + expect(result.range.end).toEqual({ + ch: 41, + line: 36 + }); + }); + it("should return the correct range of a prop when cursor is between value and unit with whitespace", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 85, line: 49}); + expect(result.range.start).toEqual({ + ch: 12, + line: 49 + }); + expect(result.range.end).toEqual({ + ch: 90, + line: 50 + }); + }); + + it("should return the correct range of a prop when cursor is at the start of whitespace of a vendor prop value w/whitespace", function () { + result = CSSUtils.getInfoAtPos(testEditor, {ch: 13, line: 49}); + expect(result.range.start).toEqual({ + ch: 12, + line: 49 + }); + expect(result.range.end).toEqual({ + ch: 90, + line: 50 + }); + }); + }); + }); + describe("CSS Regions", function () { beforeEach(function () { init(this, cssRegionsFileEntry);