Browse Source

Improve native documentation webview renderer
Make prism as a custom lib instead of node module to reduce the binary size

Geequlim 5 years ago
parent
commit
f07e1154ef
4 changed files with 593 additions and 14 deletions
  1. 0 1
      package.json
  2. 573 0
      src/deps/prism/prism.js
  3. 16 11
      src/lsp/NativeDocumentManager.ts
  4. 4 2
      tsconfig.json

+ 0 - 1
package.json

@@ -108,7 +108,6 @@
 	"dependencies": {
 	"dependencies": {
 		"global": "^4.4.0",
 		"global": "^4.4.0",
 		"marked": "^0.7.0",
 		"marked": "^0.7.0",
-		"prismjs": "^1.17.1",
 		"vscode-languageclient": "^5.2.1",
 		"vscode-languageclient": "^5.2.1",
 		"ws": "^7.0.0"
 		"ws": "^7.0.0"
 	}
 	}

+ 573 - 0
src/deps/prism/prism.js

@@ -0,0 +1,573 @@
+/* PrismJS 1.17.1
+https://prismjs.com/download.html#themes=prism */
+var _self = (typeof window !== 'undefined')
+	? window   // if in browser
+	: (
+		(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
+		? self // if in worker
+		: {}   // if in node js
+	);
+
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ */
+
+var Prism = (function (_self){
+
+// Private helper vars
+var lang = /\blang(?:uage)?-([\w-]+)\b/i;
+var uniqueId = 0;
+
+/**
+ * Returns the Prism language of the given element set by a `language-xxxx` or `lang-xxxx` class.
+ *
+ * If no language is set for the element or the element is `null` or `undefined`, `none` will be returned.
+ *
+ * @param {Element} element
+ * @returns {string}
+ */
+function getLanguage(element) {
+	while (element && !lang.test(element.className)) {
+		element = element.parentNode;
+	}
+	if (element) {
+		return (element.className.match(lang) || [, 'none'])[1].toLowerCase();
+	}
+	return 'none';
+}
+
+
+var _ = {
+	manual: _self.Prism && _self.Prism.manual,
+	disableWorkerMessageHandler: _self.Prism && _self.Prism.disableWorkerMessageHandler,
+	util: {
+		encode: function (tokens) {
+			if (tokens instanceof Token) {
+				return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
+			} else if (Array.isArray(tokens)) {
+				return tokens.map(_.util.encode);
+			} else {
+				return tokens.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
+			}
+		},
+
+		type: function (o) {
+			return Object.prototype.toString.call(o).slice(8, -1);
+		},
+
+		objId: function (obj) {
+			if (!obj['__id']) {
+				Object.defineProperty(obj, '__id', { value: ++uniqueId });
+			}
+			return obj['__id'];
+		},
+
+		// Deep clone a language definition (e.g. to extend it)
+		clone: function deepClone(o, visited) {
+			var clone, id, type = _.util.type(o);
+			visited = visited || {};
+
+			switch (type) {
+				case 'Object':
+					id = _.util.objId(o);
+					if (visited[id]) {
+						return visited[id];
+					}
+					clone = {};
+					visited[id] = clone;
+
+					for (var key in o) {
+						if (o.hasOwnProperty(key)) {
+							clone[key] = deepClone(o[key], visited);
+						}
+					}
+
+					return clone;
+
+				case 'Array':
+					id = _.util.objId(o);
+					if (visited[id]) {
+						return visited[id];
+					}
+					clone = [];
+					visited[id] = clone;
+
+					o.forEach(function (v, i) {
+						clone[i] = deepClone(v, visited);
+					});
+
+					return clone;
+
+				default:
+					return o;
+			}
+		}
+	},
+
+	languages: {
+		extend: function (id, redef) {
+			var lang = _.util.clone(_.languages[id]);
+
+			for (var key in redef) {
+				lang[key] = redef[key];
+			}
+
+			return lang;
+		},
+
+		/**
+		 * Insert a token before another token in a language literal
+		 * As this needs to recreate the object (we cannot actually insert before keys in object literals),
+		 * we cannot just provide an object, we need an object and a key.
+		 * @param inside The key (or language id) of the parent
+		 * @param before The key to insert before.
+		 * @param insert Object with the key/value pairs to insert
+		 * @param root The object that contains `inside`. If equal to Prism.languages, it can be omitted.
+		 */
+		insertBefore: function (inside, before, insert, root) {
+			root = root || _.languages;
+			var grammar = root[inside];
+			var ret = {};
+
+			for (var token in grammar) {
+				if (grammar.hasOwnProperty(token)) {
+
+					if (token == before) {
+						for (var newToken in insert) {
+							if (insert.hasOwnProperty(newToken)) {
+								ret[newToken] = insert[newToken];
+							}
+						}
+					}
+
+					// Do not insert token which also occur in insert. See #1525
+					if (!insert.hasOwnProperty(token)) {
+						ret[token] = grammar[token];
+					}
+				}
+			}
+
+			var old = root[inside];
+			root[inside] = ret;
+
+			// Update references in other language definitions
+			_.languages.DFS(_.languages, function(key, value) {
+				if (value === old && key != inside) {
+					this[key] = ret;
+				}
+			});
+
+			return ret;
+		},
+
+		// Traverse a language definition with Depth First Search
+		DFS: function DFS(o, callback, type, visited) {
+			visited = visited || {};
+
+			var objId = _.util.objId;
+
+			for (var i in o) {
+				if (o.hasOwnProperty(i)) {
+					callback.call(o, i, o[i], type || i);
+
+					var property = o[i],
+					    propertyType = _.util.type(property);
+
+					if (propertyType === 'Object' && !visited[objId(property)]) {
+						visited[objId(property)] = true;
+						DFS(property, callback, null, visited);
+					}
+					else if (propertyType === 'Array' && !visited[objId(property)]) {
+						visited[objId(property)] = true;
+						DFS(property, callback, i, visited);
+					}
+				}
+			}
+		}
+	},
+	plugins: {},
+
+	highlightAll: function(async, callback) {
+		_.highlightAllUnder(document, async, callback);
+	},
+
+	highlightAllUnder: function(container, async, callback) {
+		var env = {
+			callback: callback,
+			selector: 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
+		};
+
+		_.hooks.run('before-highlightall', env);
+
+		var elements = container.querySelectorAll(env.selector);
+
+		for (var i=0, element; element = elements[i++];) {
+			_.highlightElement(element, async === true, env.callback);
+		}
+	},
+
+	highlightElement: function(element, async, callback) {
+		// Find language
+		var language = getLanguage(element);
+		var grammar = _.languages[language];
+
+		// Set language on the element, if not present
+		element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+
+		// Set language on the parent, for styling
+		var parent = element.parentNode;
+		if (parent && parent.nodeName.toLowerCase() === 'pre') {
+			parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+		}
+
+		var code = element.textContent;
+
+		var env = {
+			element: element,
+			language: language,
+			grammar: grammar,
+			code: code
+		};
+
+		function insertHighlightedCode(highlightedCode) {
+			env.highlightedCode = highlightedCode;
+
+			_.hooks.run('before-insert', env);
+
+			env.element.innerHTML = env.highlightedCode;
+
+			_.hooks.run('after-highlight', env);
+			_.hooks.run('complete', env);
+			callback && callback.call(env.element);
+		}
+
+		_.hooks.run('before-sanity-check', env);
+
+		if (!env.code) {
+			_.hooks.run('complete', env);
+			callback && callback.call(env.element);
+			return;
+		}
+
+		_.hooks.run('before-highlight', env);
+
+		if (!env.grammar) {
+			insertHighlightedCode(_.util.encode(env.code));
+			return;
+		}
+
+		if (async && _self.Worker) {
+			var worker = new Worker(_.filename);
+
+			worker.onmessage = function(evt) {
+				insertHighlightedCode(evt.data);
+			};
+
+			worker.postMessage(JSON.stringify({
+				language: env.language,
+				code: env.code,
+				immediateClose: true
+			}));
+		}
+		else {
+			insertHighlightedCode(_.highlight(env.code, env.grammar, env.language));
+		}
+	},
+
+	highlight: function (text, grammar, language) {
+		var env = {
+			code: text,
+			grammar: grammar,
+			language: language
+		};
+		_.hooks.run('before-tokenize', env);
+		env.tokens = _.tokenize(env.code, env.grammar);
+		_.hooks.run('after-tokenize', env);
+		return Token.stringify(_.util.encode(env.tokens), env.language);
+	},
+
+	matchGrammar: function (text, strarr, grammar, index, startPos, oneshot, target) {
+		for (var token in grammar) {
+			if (!grammar.hasOwnProperty(token) || !grammar[token]) {
+				continue;
+			}
+
+			var patterns = grammar[token];
+			patterns = Array.isArray(patterns) ? patterns : [patterns];
+
+			for (var j = 0; j < patterns.length; ++j) {
+				if (target && target == token + ',' + j) {
+					return;
+				}
+
+				var pattern = patterns[j],
+					inside = pattern.inside,
+					lookbehind = !!pattern.lookbehind,
+					greedy = !!pattern.greedy,
+					lookbehindLength = 0,
+					alias = pattern.alias;
+
+				if (greedy && !pattern.pattern.global) {
+					// Without the global flag, lastIndex won't work
+					var flags = pattern.pattern.toString().match(/[imsuy]*$/)[0];
+					pattern.pattern = RegExp(pattern.pattern.source, flags + 'g');
+				}
+
+				pattern = pattern.pattern || pattern;
+
+				// Don’t cache length as it changes during the loop
+				for (var i = index, pos = startPos; i < strarr.length; pos += strarr[i].length, ++i) {
+
+					var str = strarr[i];
+
+					if (strarr.length > text.length) {
+						// Something went terribly wrong, ABORT, ABORT!
+						return;
+					}
+
+					if (str instanceof Token) {
+						continue;
+					}
+
+					if (greedy && i != strarr.length - 1) {
+						pattern.lastIndex = pos;
+						var match = pattern.exec(text);
+						if (!match) {
+							break;
+						}
+
+						var from = match.index + (lookbehind && match[1] ? match[1].length : 0),
+						    to = match.index + match[0].length,
+						    k = i,
+						    p = pos;
+
+						for (var len = strarr.length; k < len && (p < to || (!strarr[k].type && !strarr[k - 1].greedy)); ++k) {
+							p += strarr[k].length;
+							// Move the index i to the element in strarr that is closest to from
+							if (from >= p) {
+								++i;
+								pos = p;
+							}
+						}
+
+						// If strarr[i] is a Token, then the match starts inside another Token, which is invalid
+						if (strarr[i] instanceof Token) {
+							continue;
+						}
+
+						// Number of tokens to delete and replace with the new match
+						delNum = k - i;
+						str = text.slice(pos, p);
+						match.index -= pos;
+					} else {
+						pattern.lastIndex = 0;
+
+						var match = pattern.exec(str),
+							delNum = 1;
+					}
+
+					if (!match) {
+						if (oneshot) {
+							break;
+						}
+
+						continue;
+					}
+
+					if(lookbehind) {
+						lookbehindLength = match[1] ? match[1].length : 0;
+					}
+
+					var from = match.index + lookbehindLength,
+					    match = match[0].slice(lookbehindLength),
+					    to = from + match.length,
+					    before = str.slice(0, from),
+					    after = str.slice(to);
+
+					var args = [i, delNum];
+
+					if (before) {
+						++i;
+						pos += before.length;
+						args.push(before);
+					}
+
+					var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
+
+					args.push(wrapped);
+
+					if (after) {
+						args.push(after);
+					}
+
+					Array.prototype.splice.apply(strarr, args);
+
+					if (delNum != 1)
+						_.matchGrammar(text, strarr, grammar, i, pos, true, token + ',' + j);
+
+					if (oneshot)
+						break;
+				}
+			}
+		}
+	},
+
+	tokenize: function(text, grammar) {
+		var strarr = [text];
+
+		var rest = grammar.rest;
+
+		if (rest) {
+			for (var token in rest) {
+				grammar[token] = rest[token];
+			}
+
+			delete grammar.rest;
+		}
+
+		_.matchGrammar(text, strarr, grammar, 0, 0, false);
+
+		return strarr;
+	},
+
+	hooks: {
+		all: {},
+
+		add: function (name, callback) {
+			var hooks = _.hooks.all;
+
+			hooks[name] = hooks[name] || [];
+
+			hooks[name].push(callback);
+		},
+
+		run: function (name, env) {
+			var callbacks = _.hooks.all[name];
+
+			if (!callbacks || !callbacks.length) {
+				return;
+			}
+
+			for (var i=0, callback; callback = callbacks[i++];) {
+				callback(env);
+			}
+		}
+	},
+
+	Token: Token
+};
+
+_self.Prism = _;
+
+function Token(type, content, alias, matchedStr, greedy) {
+	this.type = type;
+	this.content = content;
+	this.alias = alias;
+	// Copy of the full string this token was created from
+	this.length = (matchedStr || '').length|0;
+	this.greedy = !!greedy;
+}
+
+Token.stringify = function(o, language) {
+	if (typeof o == 'string') {
+		return o;
+	}
+
+	if (Array.isArray(o)) {
+		return o.map(function(element) {
+			return Token.stringify(element, language);
+		}).join('');
+	}
+
+	var env = {
+		type: o.type,
+		content: Token.stringify(o.content, language),
+		tag: 'span',
+		classes: ['token', o.type],
+		attributes: {},
+		language: language
+	};
+
+	if (o.alias) {
+		var aliases = Array.isArray(o.alias) ? o.alias : [o.alias];
+		Array.prototype.push.apply(env.classes, aliases);
+	}
+
+	_.hooks.run('wrap', env);
+
+	var attributes = Object.keys(env.attributes).map(function(name) {
+		return name + '="' + (env.attributes[name] || '').replace(/"/g, '&quot;') + '"';
+	}).join(' ');
+
+	return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + '</' + env.tag + '>';
+};
+
+if (!_self.document) {
+	if (!_self.addEventListener) {
+		// in Node.js
+		return _;
+	}
+
+	if (!_.disableWorkerMessageHandler) {
+		// In worker
+		_self.addEventListener('message', function (evt) {
+			var message = JSON.parse(evt.data),
+				lang = message.language,
+				code = message.code,
+				immediateClose = message.immediateClose;
+
+			_self.postMessage(_.highlight(code, _.languages[lang], lang));
+			if (immediateClose) {
+				_self.close();
+			}
+		}, false);
+	}
+
+	return _;
+}
+
+//Get current script and highlight
+var script = document.currentScript || [].slice.call(document.getElementsByTagName('script')).pop();
+
+if (script) {
+	_.filename = script.src;
+	
+	if (script.hasAttribute('data-manual')) {
+		_.manual = true;
+	}
+}
+
+if (!_.manual) {
+	function highlightAutomaticallyCallback() {
+		if (!_.manual) {
+			_.highlightAll();
+		}
+	}
+
+	if(document.readyState !== 'loading') {
+		if (window.requestAnimationFrame) {
+			window.requestAnimationFrame(highlightAutomaticallyCallback);
+		} else {
+			window.setTimeout(highlightAutomaticallyCallback, 16);
+		}
+	}
+	else {
+		document.addEventListener('DOMContentLoaded', highlightAutomaticallyCallback);
+	}
+}
+
+return _;
+
+})(_self);
+
+if (typeof module !== 'undefined' && module.exports) {
+	module.exports = Prism;
+}
+
+// hack for components to work correctly in node.js
+if (typeof global !== 'undefined') {
+	global.Prism = Prism;
+}
+;

+ 16 - 11
src/lsp/NativeDocumentManager.ts

@@ -3,14 +3,14 @@ import { EventEmitter } from "events";
 import { MessageIO } from "./MessageIO";
 import { MessageIO } from "./MessageIO";
 import { NotificationMessage } from "vscode-jsonrpc";
 import { NotificationMessage } from "vscode-jsonrpc";
 import { DocumentSymbol } from "vscode";
 import { DocumentSymbol } from "vscode";
-import * as Prism from "prismjs";
+import * as Prism from "../deps/prism/prism";
 import * as marked from "marked";
 import * as marked from "marked";
 marked.setOptions({
 marked.setOptions({
 	highlight: function (code, lang) {
 	highlight: function (code, lang) {
 		return Prism.highlight(code, GDScriptGrammar, lang);
 		return Prism.highlight(code, GDScriptGrammar, lang);
 	}
 	}
 });
 });
-				
+
 const enum Methods {
 const enum Methods {
 	SHOW_NATIVE_SYMBOL = 'gdscript/show_native_symbol',
 	SHOW_NATIVE_SYMBOL = 'gdscript/show_native_symbol',
 	INSPECT_NATIVE_SYMBOL = 'textDocument/nativeSymbol'
 	INSPECT_NATIVE_SYMBOL = 'textDocument/nativeSymbol'
@@ -120,17 +120,18 @@ export default class NativeDocumentManager extends EventEmitter {
 	
 	
 	
 	
 	private make_symbol_document(symbol: GodotNativeSymbol): string {
 	private make_symbol_document(symbol: GodotNativeSymbol): string {
+		const classlink = make_link(symbol.native_class, undefined);
 		
 		
-		function make_function_signature(s: GodotNativeSymbol) {
+		function make_function_signature(s: GodotNativeSymbol, with_class = false) {
 			let parts = /\((.*)?\)\s*\-\>\s*(([A-z0-9]+)?)$/.exec(s.detail);
 			let parts = /\((.*)?\)\s*\-\>\s*(([A-z0-9]+)?)$/.exec(s.detail);
 			if (!parts) return "";
 			if (!parts) return "";
 			const ret_type = make_link(parts[2] || "void", undefined);
 			const ret_type = make_link(parts[2] || "void", undefined);
 			let args = (parts[1] || "").replace(/\:\s([A-z0-9_]+)(\,\s*)?/g, `: <a href="" onclick="inspect('$1', '$1')">$1</a>$2`);
 			let args = (parts[1] || "").replace(/\:\s([A-z0-9_]+)(\,\s*)?/g, `: <a href="" onclick="inspect('$1', '$1')">$1</a>$2`);
 			args = args.replace(/\s=\s(.*?)[\,\)]/g, "")
 			args = args.replace(/\s=\s(.*?)[\,\)]/g, "")
-			return `${ret_type} ${element("a", s.name, {href: `#${s.name}`})}( ${args} )`;
+			return `${ret_type} ${with_class?`${classlink}.`:''}${element("a", s.name, {href: `#${s.name}`})}( ${args} )`;
 		};
 		};
 		
 		
-		function make_symbol_elements(s: GodotNativeSymbol): {index?: string, body: string} {
+		function make_symbol_elements(s: GodotNativeSymbol, with_class = false): {index?: string, body: string} {
 			switch (s.kind) {
 			switch (s.kind) {
 				case vscode.SymbolKind.Property:
 				case vscode.SymbolKind.Property:
 				case vscode.SymbolKind.Variable: {
 				case vscode.SymbolKind.Variable: {
@@ -139,7 +140,7 @@ export default class NativeDocumentManager extends EventEmitter {
 					if (!parts) return;
 					if (!parts) return;
 					let type = make_link(parts[2], undefined);
 					let type = make_link(parts[2], undefined);
 					let name = element("a", s.name, {href: `#${s.name}`});
 					let name = element("a", s.name, {href: `#${s.name}`});
-					const title = element('h4', type + " " + s.name);
+					const title = element('h4', `${type} ${with_class?`${classlink}.`:''}${s.name}`);
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const div = element("div", title + doc);
 					const div = element("div", title + doc);
 					return {
 					return {
@@ -156,7 +157,7 @@ export default class NativeDocumentManager extends EventEmitter {
 					let name = parts[1];
 					let name = parts[1];
 					let value = element('code', parts[4]);
 					let value = element('code', parts[4]);
 					
 					
-					const title = element('p', type + " " + name + " = " + value);
+					const title = element('p', `${type} ${with_class?`${classlink}.`:''}${name} = ${value}`);
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const div = element("div", title + doc);
 					const div = element("div", title + doc);
 					return {
 					return {
@@ -167,7 +168,7 @@ export default class NativeDocumentManager extends EventEmitter {
 					const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
 					const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
 					if (!parts) return;
 					if (!parts) return;
 					const args = (parts[2] || "").replace(/\:\s([A-z0-9_]+)(\,\s*)?/g, `: <a href="" onclick="inspect('$1', '$1')">$1</a>$2`);
 					const args = (parts[2] || "").replace(/\:\s([A-z0-9_]+)(\,\s*)?/g, `: <a href="" onclick="inspect('$1', '$1')">$1</a>$2`);
-					const title = element('p', `${s.name}( ${args} )`);
+					const title = element('p', `${with_class?`signal ${with_class?`${classlink}.`:''}`:''}${s.name}( ${args} )`);
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const div = element("div", title + doc);
 					const div = element("div", title + doc);
 					return {
 					return {
@@ -176,7 +177,7 @@ export default class NativeDocumentManager extends EventEmitter {
 				} break;
 				} break;
 				case vscode.SymbolKind.Method:
 				case vscode.SymbolKind.Method:
 				case vscode.SymbolKind.Function: {
 				case vscode.SymbolKind.Function: {
-					const signature = make_function_signature(s);
+					const signature = make_function_signature(s, with_class);
 					const title = element("h4", signature);
 					const title = element("h4", signature);
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
 					const div = element("div", title + doc);
 					const div = element("div", title + doc);
@@ -239,6 +240,8 @@ export default class NativeDocumentManager extends EventEmitter {
 					doc += element('ul', block);
 					doc += element('ul', block);
 				}
 				}
 			};
 			};
+			
+			doc += element("p", format_documentation(symbol.documentation, symbol.native_class));
 			add_group("Properties", properties_index);
 			add_group("Properties", properties_index);
 			add_group("Constants", constants);
 			add_group("Constants", constants);
 			add_group("Signals", signals);
 			add_group("Signals", signals);
@@ -251,9 +254,11 @@ export default class NativeDocumentManager extends EventEmitter {
 			return doc;
 			return doc;
 		} else {
 		} else {
 			let doc = "";
 			let doc = "";
-			const elements = make_symbol_elements(symbol);
+			const elements = make_symbol_elements(symbol, true);
 			if (elements.index) {
 			if (elements.index) {
-				doc += element("h2", elements.index);
+				if ([vscode.SymbolKind.Function, vscode.SymbolKind.Method].indexOf(symbol.kind) == -1) {
+					doc += element("h2", elements.index);
+				}
 			}
 			}
 			doc += element("div", elements.body);
 			doc += element("div", elements.body);
 			return doc;
 			return doc;

+ 4 - 2
tsconfig.json

@@ -9,9 +9,11 @@
 		],
 		],
 		"sourceMap": true,
 		"sourceMap": true,
 		"rootDir": "src",
 		"rootDir": "src",
-		"strict": false
+		"strict": false,
+		"allowJs": true
 	},
 	},
 	"exclude": [
 	"exclude": [
-		"node_modules"
+		"node_modules",
+		"out"
 	]
 	]
 }
 }