Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit ba96598

Browse files
committed
Merge pull request #10294 from sprintr/master
SVG Code Hints (#10084)
2 parents 897fd8d + 4b927bd commit ba96598

File tree

9 files changed

+2334
-0
lines changed

9 files changed

+2334
-0
lines changed

src/brackets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ define(function (require, exports, module) {
109109
require("utils/ColorUtils");
110110
require("view/ThemeManager");
111111
require("thirdparty/lodash");
112+
require("language/XMLUtils");
112113

113114
// DEPRECATED: In future we want to remove the global CodeMirror, but for now we
114115
// expose our required CodeMirror globally so as to avoid breaking extensions in the

src/extensions/default/SVGCodeHints/SVGAttributes.json

Lines changed: 369 additions & 0 deletions
Large diffs are not rendered by default.

src/extensions/default/SVGCodeHints/SVGTags.json

Lines changed: 330 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/*
2+
* Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*
22+
*/
23+
24+
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
25+
/*global define, brackets, $ */
26+
27+
define(function (require, exports, module) {
28+
"use strict";
29+
30+
// Load dependencies.
31+
var AppInit = brackets.getModule("utils/AppInit"),
32+
CodeHintManager = brackets.getModule("editor/CodeHintManager"),
33+
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
34+
XMLUtils = brackets.getModule("language/XMLUtils"),
35+
StringMatch = brackets.getModule("utils/StringMatch"),
36+
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
37+
SVGTags = require("text!SVGTags.json"),
38+
SVGAttributes = require("text!SVGAttributes.json"),
39+
cachedAttributes = {},
40+
tagData,
41+
attributeData,
42+
isSVGEnabled;
43+
44+
var stringMatcherOptions = {
45+
preferPrefixMatches: true
46+
};
47+
48+
// Define our own pref for hinting.
49+
PreferencesManager.definePreference("codehint.SVGHints", "boolean", true);
50+
51+
// Preferences to control hint.
52+
function _isSVGHintsEnabled() {
53+
return (PreferencesManager.get("codehint.SVGHints") !== false &&
54+
PreferencesManager.get("showCodeHints") !== false);
55+
}
56+
57+
PreferencesManager.on("change", "codehint.SVGHints", function () {
58+
isSVGEnabled = _isSVGHintsEnabled();
59+
});
60+
61+
PreferencesManager.on("change", "showCodeHints", function () {
62+
isSVGEnabled = _isSVGHintsEnabled();
63+
});
64+
65+
// Check if SVG Hints are available.
66+
isSVGEnabled = _isSVGHintsEnabled();
67+
68+
/**
69+
* Returns a list of attributes used by a tag.
70+
*
71+
* @param {string} tagName name of the SVG tag.
72+
* @return {Array.<string>} list of attributes.
73+
*/
74+
function getTagAttributes(tagName) {
75+
var tag;
76+
77+
if (!cachedAttributes.hasOwnProperty(tagName)) {
78+
tag = tagData.tags[tagName];
79+
cachedAttributes[tagName] = [];
80+
if (tag.attributes) {
81+
cachedAttributes[tagName] = cachedAttributes[tagName].concat(tag.attributes);
82+
}
83+
tag.attributeGroups.forEach(function (group) {
84+
if (tagData.attributeGroups.hasOwnProperty(group)) {
85+
cachedAttributes[tagName] = cachedAttributes[tagName].concat(tagData.attributeGroups[group]);
86+
}
87+
});
88+
cachedAttributes[tagName].sort();
89+
}
90+
return cachedAttributes[tagName];
91+
}
92+
93+
/*
94+
* Returns a sorted and formatted list of hints with the query substring
95+
* highlighted.
96+
*
97+
* @param {Array.<Object>} hints - the list of hints to format
98+
* @param {string} query - querystring used for highlighting matched
99+
* poritions of each hint
100+
* @return {Array.jQuery} sorted Array of jQuery DOM elements to insert
101+
*/
102+
function formatHints(hints, query) {
103+
StringMatch.basicMatchSort(hints);
104+
return hints.map(function (token) {
105+
var $hintObj = $("<span>").addClass("brackets-svg-hints");
106+
107+
// highlight the matched portion of each hint
108+
if (token.stringRanges) {
109+
token.stringRanges.forEach(function (item) {
110+
if (item.matched) {
111+
$hintObj.append($("<span>")
112+
.text(item.text)
113+
.addClass("matched-hint"));
114+
} else {
115+
$hintObj.append(item.text);
116+
}
117+
});
118+
} else {
119+
$hintObj.text(token.value);
120+
}
121+
122+
$hintObj.data("token", token);
123+
124+
return $hintObj;
125+
});
126+
}
127+
128+
/**
129+
* @constructor
130+
*/
131+
function SVGCodeHints() {
132+
this.tagInfo = null;
133+
}
134+
135+
/**
136+
* Determines whether SVG code hints are available in the current editor.
137+
*
138+
* @param {!Editor} editor An instance of Editor
139+
* @param {string} implicitChar A single character that was inserted by the
140+
* user or null if the request was explicity made to start hinting session.
141+
*
142+
* @return {boolean} Determines whether or not hints are available in the current context.
143+
*/
144+
SVGCodeHints.prototype.hasHints = function (editor, implicitChar) {
145+
if (isSVGEnabled && editor.getModeForSelection() === "image/svg+xml") {
146+
this.editor = editor;
147+
this.tagInfo = XMLUtils.getTagInfo(this.editor, this.editor.getCursorPos());
148+
149+
if (this.tagInfo && this.tagInfo.tokenType) {
150+
return true;
151+
}
152+
}
153+
return false;
154+
};
155+
156+
/**
157+
* Returns a list of hints that are available in the current context,
158+
* or null if there are no hints available.
159+
*
160+
* @param {string} implicitChar A character that the user typed in the hinting session.
161+
* @return {!{hints: Array.<jQueryObject>, match: string, selectInitial: boolean, handleWideResults: boolean}}
162+
*/
163+
SVGCodeHints.prototype.getHints = function (implicitChar) {
164+
var hints = [], query, tagInfo, attributes = [], options = [], index, isMultiple, tagSpecificOptions;
165+
166+
tagInfo = XMLUtils.getTagInfo(this.editor, this.editor.getCursorPos());
167+
this.tagInfo = tagInfo;
168+
169+
if (tagInfo && tagInfo.tokenType) {
170+
query = tagInfo.token.string.substr(0, tagInfo.offset).trim();
171+
172+
if (tagInfo.tokenType === XMLUtils.TOKEN_TAG) {
173+
hints = $.map(Object.keys(tagData.tags), function (tag) {
174+
var match = StringMatch.stringMatch(tag, query, stringMatcherOptions);
175+
if (match) {
176+
return match;
177+
}
178+
});
179+
} else if (tagInfo.tokenType === XMLUtils.TOKEN_ATTR) {
180+
if (!tagData.tags[tagInfo.tagName]) {
181+
return null;
182+
}
183+
// Get attributes.
184+
attributes = getTagAttributes(tagInfo.tagName);
185+
hints = $.map(attributes, function (attribute) {
186+
if (tagInfo.exclusionList.indexOf(attribute) === -1) {
187+
var match = StringMatch.stringMatch(attribute, query, stringMatcherOptions);
188+
if (match) {
189+
return match;
190+
}
191+
}
192+
});
193+
} else if (tagInfo.tokenType === XMLUtils.TOKEN_VALUE) {
194+
index = tagInfo.tagName + "/" + tagInfo.attrName;
195+
tagSpecificOptions = attributeData[index];
196+
197+
if (!tagData.tags[tagInfo.tagName] && !(attributeData[tagInfo.attrName] || tagSpecificOptions)) {
198+
return null;
199+
}
200+
201+
// Get attribute options.
202+
// Prefer tag/attribute for specific tags, else use general options for attributes.
203+
if (tagSpecificOptions) {
204+
options = tagSpecificOptions.attribOptions;
205+
isMultiple = tagSpecificOptions.multiple;
206+
} else if (attributeData[tagInfo.attrName]) {
207+
options = attributeData[tagInfo.attrName].attribOptions;
208+
isMultiple = attributeData[tagInfo.attrName].multiple;
209+
}
210+
211+
// Stop if the attribute doesn't support multiple options.
212+
if (!isMultiple && /\s+/.test(tagInfo.token.string)) {
213+
return null;
214+
}
215+
216+
query = XMLUtils.getValueQuery(tagInfo);
217+
hints = $.map(options, function (option) {
218+
if (tagInfo.exclusionList.indexOf(option) === -1) {
219+
var match = StringMatch.stringMatch(option, query, stringMatcherOptions);
220+
if (match) {
221+
return match;
222+
}
223+
}
224+
});
225+
}
226+
return {
227+
hints: formatHints(hints, query),
228+
match: null,
229+
selectInitial: true,
230+
handleWideResults: false
231+
};
232+
}
233+
return null;
234+
};
235+
236+
/**
237+
* Insert the selected hint into the editor
238+
*
239+
* @param {string} completion The string that user selected from the list
240+
* @return {boolean} Determines whether or not to continue the hinting session
241+
*/
242+
SVGCodeHints.prototype.insertHint = function (completion) {
243+
var tagInfo = this.tagInfo,
244+
pos = this.editor.getCursorPos(),
245+
start = {line: -1, ch: -1},
246+
end = {line: -1, ch: -1},
247+
query,
248+
startChar,
249+
endChar,
250+
quoteChar;
251+
252+
if (completion.jquery) {
253+
completion = completion.text();
254+
}
255+
start.line = end.line = pos.line;
256+
257+
if (tagInfo.tokenType === XMLUtils.TOKEN_TAG) {
258+
start.ch = pos.ch - tagInfo.offset;
259+
end.ch = tagInfo.token.end;
260+
this.editor.document.replaceRange(completion, start, end);
261+
return false;
262+
} else if (tagInfo.tokenType === XMLUtils.TOKEN_ATTR) {
263+
if (!tagInfo.shouldReplace) {
264+
completion += "=\"\"";
265+
266+
// In case the current token is whitespace, start and end will be same.
267+
if (XMLUtils.regexWhitespace.test(tagInfo.token.string)) {
268+
start.ch = end.ch = pos.ch;
269+
} else {
270+
start.ch = pos.ch - tagInfo.offset;
271+
end.ch = pos.ch;
272+
}
273+
this.editor.document.replaceRange(completion, start, end);
274+
this.editor.setCursorPos(start.line, start.ch + completion.length - 1);
275+
return true;
276+
} else {
277+
// We don't append ="" again, just replace the attribute token.
278+
start.ch = tagInfo.token.start;
279+
end.ch = tagInfo.token.end;
280+
this.editor.document.replaceRange(completion, start, end);
281+
this.editor.setCursorPos(start.line, start.ch + completion.length);
282+
return false;
283+
}
284+
} else if (tagInfo.tokenType === XMLUtils.TOKEN_VALUE) {
285+
startChar = tagInfo.token.string.charAt(0);
286+
endChar = tagInfo.token.string.substr(-1, 1);
287+
288+
// Get the quote character.
289+
if (/^['"]$/.test(startChar)) {
290+
quoteChar = startChar;
291+
} else {
292+
quoteChar = "\"";
293+
}
294+
295+
// Append quotes to attribute value if not already.
296+
if (!/^['"]$/.test(startChar)) {
297+
completion = quoteChar + completion;
298+
}
299+
if (!/^['"]$/.test(endChar) || tagInfo.token.string.length === 1) {
300+
completion = completion + quoteChar;
301+
}
302+
303+
query = XMLUtils.getValueQuery(tagInfo);
304+
start.ch = pos.ch - query.length;
305+
end.ch = pos.ch;
306+
this.editor.document.replaceRange(completion, start, end);
307+
308+
// Place cursor outside the quote if the next char is quote.
309+
if (/^['"]$/.test(tagInfo.token.string.substr(tagInfo.offset, 1))) {
310+
this.editor.setCursorPos(pos.line, start.ch + completion.length + 1);
311+
}
312+
return false;
313+
}
314+
};
315+
316+
AppInit.appReady(function () {
317+
tagData = JSON.parse(SVGTags);
318+
attributeData = JSON.parse(SVGAttributes);
319+
320+
var hintProvider = new SVGCodeHints();
321+
CodeHintManager.registerHintProvider(hintProvider, ["svg"], 0);
322+
323+
ExtensionUtils.loadStyleSheet(module, "styles/brackets-svg-hints.css");
324+
exports.hintProvider = hintProvider;
325+
});
326+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*
22+
*/
23+
24+
.brackets-svg-hints .matched-hint {
25+
font-weight: 500;
26+
color: #000;
27+
}
28+
.dark .brackets-svg-hints .matched-hint {
29+
color: #ccc;
30+
}

0 commit comments

Comments
 (0)