Browse Source

Render docs of native symbols in webview mostly works now

geequlim 6 years ago
parent
commit
758aafc570
3 changed files with 433 additions and 32 deletions
  1. 7 3
      package.json
  2. 423 27
      src/lsp/NativeDocumentManager.ts
  3. 3 2
      tsconfig.json

+ 7 - 3
package.json

@@ -96,16 +96,20 @@
 		]
 	},
 	"devDependencies": {
+		"@types/marked": "^0.6.5",
 		"@types/mocha": "^2.2.42",
 		"@types/node": "^10.12.21",
+		"@types/prismjs": "^1.16.0",
 		"@types/ws": "^6.0.1",
 		"tslint": "^5.16.0",
-		"vscode": "^1.1.33",
-		"typescript": "^3.4.5"
+		"typescript": "^3.4.5",
+		"vscode": "^1.1.33"
 	},
 	"dependencies": {
-		"vscode-languageclient": "^5.2.1",
 		"global": "^4.4.0",
+		"marked": "^0.7.0",
+		"prismjs": "^1.17.1",
+		"vscode-languageclient": "^5.2.1",
 		"ws": "^7.0.0"
 	}
 }

+ 423 - 27
src/lsp/NativeDocumentManager.ts

@@ -3,7 +3,14 @@ import { EventEmitter } from "events";
 import { MessageIO } from "./MessageIO";
 import { NotificationMessage } from "vscode-jsonrpc";
 import { DocumentSymbol } from "vscode";
-
+import * as Prism from "prismjs";
+import * as marked from "marked";
+marked.setOptions({
+	highlight: function (code, lang) {
+		return Prism.highlight(code, GDScriptGrammar, lang);
+	}
+});
+				
 const enum Methods {
 	SHOW_NATIVE_SYMBOL = 'gdscript/show_native_symbol',
 	INSPECT_NATIVE_SYMBOL = 'textDocument/nativeSymbol'
@@ -76,44 +83,433 @@ export default class NativeDocumentManager extends EventEmitter {
 	private make_html_content(symbol: GodotNativeSymbol): string {
 		return `
 		<html>
-			<body>${this.make_symbol_document(symbol)}</body>
+			<head>
+				<style type="text/css">
+					${PrismStyleSheet}
+					.codeblock {
+						padding: 0.5em;
+						margin: .5em 0;
+						overflow: auto;
+						border-radius: 0.3em;
+						!background-color: #fdf6e3;
+					}
+					a {
+						text-decoration: none;
+					}
+				</style>
+			</head>
+			<body style="line-height: 16pt;">${this.make_symbol_document(symbol)}</body>
 			<script>
 				var vscode = acquireVsCodeApi();
 				function inspect(native_class, symbol_name) {
-					vscode.postMessage({
-						type: '${WebViewMessageType.INSPECT_NATIVE_SYMBOL}',
-						data: {
-							native_class: native_class,
-							symbol_name: symbol_name
-						}
-					});
+					if (typeof(godot_class) != 'undefined' && godot_class == native_class) {
+						document.getElementById(symbol_name).scrollIntoView();
+					} else {
+						vscode.postMessage({
+							type: '${WebViewMessageType.INSPECT_NATIVE_SYMBOL}',
+							data: {
+								native_class: native_class,
+								symbol_name: symbol_name
+							}
+						});
+					}
 				};
 			</script>
 		</html>`;
 	}
 	
+	
 	private make_symbol_document(symbol: GodotNativeSymbol): string {
-		let doc = '';
-		function line(text: string) {
-			doc += text + '\n';
+		
+		function make_function_signature(s: GodotNativeSymbol) {
+			let parts = /\((.*)?\)\s*\-\>\s*(([A-z0-9]+)?)$/.exec(s.detail);
+			if (!parts) return "";
+			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`);
+			args = args.replace(/\s=\s(.*?)[\,\)]/g, "")
+			return `${ret_type} ${element("a", s.name, {href: `#${s.name}`})}( ${args} )`;
 		};
 		
-		switch (symbol.kind) {
-			case vscode.SymbolKind.Class: {
-				line(`<h1>${symbol.detail}</h1>`);
-				line(`<h3>Description</h3>`)
-				line(`<p>${this.parse_markdown(symbol.documentation)}</p>`);
-				line(`<a onclick="inspect('Control', 'rect_position')">Control.rect_position</a>`);
-			} break;
-			default:
-				line(`<h1>${symbol.detail}</h1>`);
-				line(`<p>${this.parse_markdown(symbol.documentation)}</p>`);
-				break;
+		function make_symbol_elements(s: GodotNativeSymbol): {index?: string, body: string} {
+			switch (s.kind) {
+				case vscode.SymbolKind.Property:
+				case vscode.SymbolKind.Variable: {
+					// var Control.anchor_left: float
+					const parts = /\.([A-z_0-9]+)\:\s(.*)$/.exec(s.detail);
+					if (!parts) return;
+					let type = make_link(parts[2], undefined);
+					let name = element("a", s.name, {href: `#${s.name}`});
+					const title = element('h4', type + " " + s.name);
+					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
+					const div = element("div", title + doc);
+					return {
+						index: type + " " + name,
+						body: div,
+					};
+				} break;
+				case vscode.SymbolKind.Constant: {
+					// const Control.FOCUS_ALL: FocusMode = 2
+					// const Control.NOTIFICATION_RESIZED = 40
+					const parts = /\.([A-Za-z_0-9]+)(\:\s*)?([A-z0-9_\.]+)?\s*=\s*(.*)$/.exec(s.detail);
+					if (!parts) return;
+					let type = make_link(parts[3] || 'int', undefined);
+					let name = parts[1];
+					let value = element('code', parts[4]);
+					
+					const title = element('p', type + " " + name + " = " + value);
+					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
+					const div = element("div", title + doc);
+					return {
+						body: div
+					};
+				} break;
+				case vscode.SymbolKind.Event: {
+					const parts = /\.([A-z0-9]+)\((.*)?\)/.exec(s.detail);
+					if (!parts) return;
+					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 doc = element("p", format_documentation(s.documentation, symbol.native_class));
+					const div = element("div", title + doc);
+					return {
+						body: div
+					};
+				} break;
+				case vscode.SymbolKind.Method:
+				case vscode.SymbolKind.Function: {
+					const signature = make_function_signature(s);
+					const title = element("h4", signature);
+					const doc = element("p", format_documentation(s.documentation, symbol.native_class));
+					const div = element("div", title + doc);
+					return {
+						index: signature,
+						body: div
+					};
+				} break;
+				default:
+					break;
+			}
+		};
+		
+		if (symbol.kind == vscode.SymbolKind.Class) {
+			
+			let doc = element("h2", `Native class ${symbol.name}`);
+			const parts = /extends\s+([A-z0-9]+)/.exec(symbol.detail);
+			let inherits = parts && parts.length > 1 ? parts[1] : '';
+			if (inherits) {
+				inherits = `Inherits ${make_link(inherits, undefined)}`;
+				doc += element("p", inherits);
+			}
+			
+			let constants = "";
+			let signals = "";
+			let methods_index = "";
+			let methods = "";
+			let properties_index = "";
+			let propertyies = "";
+			let others = "";
+			
+			for (let s of symbol.children as GodotNativeSymbol[]) {
+				const elements = make_symbol_elements(s);
+				switch (s.kind) {
+					case vscode.SymbolKind.Property:
+					case vscode.SymbolKind.Variable:
+						properties_index += element("li", elements.index);
+						propertyies += element("li", elements.body, {id: s.name});
+						break;
+					case vscode.SymbolKind.Constant:
+						constants += element("li", elements.body, {id: s.name});
+						break;
+					case vscode.SymbolKind.Event:
+						signals += element("li", elements.body, {id: s.name});
+						break;
+					case vscode.SymbolKind.Method:
+					case vscode.SymbolKind.Function:
+						methods_index += element("li", elements.index);
+						methods += element("li", elements.body, {id: s.name});
+						break;
+					default:
+						others += element("li", elements.body, {id: s.name});
+						break;
+				}
+			}
+			
+			function add_group(title: string, block: string) {
+				if (block) {
+					doc += element('h3', title);
+					doc += element('ul', block);
+				}
+			};
+			add_group("Properties", properties_index);
+			add_group("Constants", constants);
+			add_group("Signals", signals);
+			add_group("Methods", methods_index);
+			add_group("Property Descriptions", propertyies);
+			add_group("Method Descriptions", methods);
+			add_group("Other Members", others);
+			doc += element("script", `var godot_class = "${symbol.native_class}";`);
+			
+			return doc;
+		} else {
+			let doc = "";
+			const elements = make_symbol_elements(symbol);
+			if (elements.index) {
+				doc += element("h2", elements.index);
+			}
+			doc += element("div", elements.body);
+			return doc;
 		}
-		return doc;
 	}
-	
-	private parse_markdown(markdown: string): string {
-		return markdown;
+}
+
+function element<K extends keyof HTMLElementTagNameMap>(tag: K, content: string, props = {}, new_line?: boolean, indent?:string) {
+	let props_str = "";
+	for (const key in props) {
+		if (props.hasOwnProperty(key)) {
+			props_str += ` ${key}="${props[key]}"`;
+		}
+	}
+	return `${indent || ''}<${tag} ${props_str}>${content}</${tag}>${new_line ? '\n' : ''}`;
+}
+function make_link(classname: string, symbol: string) {
+	if (!symbol || symbol == classname) {
+		return element('a', classname, {onclick: `inspect('${classname}', '${classname}')`, href: ''});
+	} else {
+		return element('a', `${classname}.${symbol}`, {onclick: `inspect('${classname}', '${symbol}')`, href: ''});
 	}
 }
+
+function make_codeblock(code: string) {
+	const md = marked('```gdscript\n' + code + '\n```');
+	return `<div class="codeblock">${md}</div>`;
+}
+
+function format_documentation(p_bbcode: string, classname: string) {
+	let html = p_bbcode.trim();
+	let lines = html.split("\n");
+	let in_code_block = false;
+	let code_block_indent = -1;
+	let cur_code_block = "";
+
+	html = "";
+	for (let i = 0; i < lines.length; i++) {
+		let line = lines[i];
+		let block_start = line.indexOf("[codeblock]");
+		if (block_start != -1) {
+			code_block_indent = block_start;
+			in_code_block = true;
+			line = line.replace("[codeblock]", "");
+		} else if (in_code_block) {
+			line = line.substr(code_block_indent, line.length);
+		}
+
+		if (in_code_block && line.indexOf("[/codeblock]") != -1) {
+			line = line.replace("[/codeblock]", "");
+			in_code_block = false;
+			html += make_codeblock(cur_code_block);
+			cur_code_block = "";
+		}
+
+		if (!in_code_block) {
+			line = line.trim();
+			// [i] [/u] [code] --> <i> </u> <code>
+			line = line.replace(/(\[(\/?)([a-z]+)\])/g, `<$2$3>`);
+			// [Reference] --> <a>Reference</a>
+			line = line.replace(/(\[([A-Z]+[A-Z_a-z0-9]*)\])/g, `<a href="" onclick="inspect('$2', '$2')">$2</a>`);
+			// [method _set] --> <a>_set</a>
+			line = line.replace(/(\[([a-z]+)\s+([A-Z_a-z][A-Z_a-z0-9]*)\])/g, `<a href="" onclick="inspect('${classname}', '$3')">$3</a>`);
+			line += "<br/>";
+			html += line;
+		} else {
+			line += "\n";
+			if (cur_code_block || line.trim()) {
+				cur_code_block += line;
+			}
+		}
+	}
+	return html;
+}
+
+
+const GDScriptGrammar = {
+	'comment': {
+		pattern: /(^|[^\\])#.*/,
+		lookbehind: true
+	},
+	'string-interpolation': {
+		pattern: /(?:f|rf|fr)(?:("""|''')[\s\S]+?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,
+		greedy: true,
+		inside: {
+			'interpolation': {
+				// "{" <expression> <optional "!s", "!r", or "!a"> <optional ":" format specifier> "}"
+				pattern: /((?:^|[^{])(?:{{)*){(?!{)(?:[^{}]|{(?!{)(?:[^{}]|{(?!{)(?:[^{}])+})+})+}/,
+				lookbehind: true,
+				inside: {
+					'format-spec': {
+						pattern: /(:)[^:(){}]+(?=}$)/,
+						lookbehind: true
+					},
+					'conversion-option': {
+						pattern: /![sra](?=[:}]$)/,
+						alias: 'punctuation'
+					},
+					rest: null
+				}
+			},
+			'string': /[\s\S]+/
+		}
+	},
+	'triple-quoted-string': {
+		pattern: /(?:[rub]|rb|br)?("""|''')[\s\S]+?\1/i,
+		greedy: true,
+		alias: 'string'
+	},
+	'string': {
+		pattern: /(?:[rub]|rb|br)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,
+		greedy: true
+	},
+	'function': {
+		pattern: /((?:^|\s)func[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,
+		lookbehind: true
+	},
+	'class-name': {
+		pattern: /(\bclass\s+)\w+/i,
+		lookbehind: true
+	},
+	'decorator': {
+		pattern: /(^\s*)@\w+(?:\.\w+)*/im,
+		lookbehind: true,
+		alias: ['annotation', 'punctuation'],
+		inside: {
+			'punctuation': /\./
+		}
+	},
+	'keyword': /\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
+	'builtin': /\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
+	'boolean': /\b(?:true|false)\b/,
+	'number': /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
+	'operator': /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,
+	'punctuation': /[{}[\];(),.:]/
+};
+
+const PrismStyleSheet = `
+code[class*="language-"],
+pre[class*="language-"] {
+	color: #657b83; /* base00 */
+	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+	font-size: 1em;
+	text-align: left;
+	white-space: pre;
+	word-spacing: normal;
+	word-break: normal;
+	word-wrap: normal;
+
+	line-height: 1.5;
+
+	-moz-tab-size: 4;
+	-o-tab-size: 4;
+	tab-size: 4;
+
+	-webkit-hyphens: none;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
+	background: #073642; /* base02 */
+}
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+code[class*="language-"]::selection, code[class*="language-"] ::selection {
+	background: #073642; /* base02 */
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+	padding: 1em;
+	margin: .5em 0;
+	overflow: auto;
+	border-radius: 0.3em;
+}
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+	background-color: #fdf6e3; /* base3 */
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+	padding: .1em;
+	border-radius: .3em;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+	color: #93a1a1; /* base1 */
+}
+
+.token.punctuation {
+	color: #586e75; /* base01 */
+}
+
+.namespace {
+	opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+	color: #268bd2; /* blue */
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.url,
+.token.inserted {
+	color: #2aa198; /* cyan */
+}
+
+.token.entity {
+	color: #657b83; /* base00 */
+	background: #eee8d5; /* base2 */
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+	color: #859900; /* green */
+}
+
+.token.function,
+.token.class-name {
+	color: #b58900; /* yellow */
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+	color: #cb4b16; /* orange */
+}
+
+.token.important,
+.token.bold {
+	font-weight: bold;
+}
+.token.italic {
+	font-style: italic;
+}
+
+.token.entity {
+	cursor: help;
+}
+`;

+ 3 - 2
tsconfig.json

@@ -4,13 +4,14 @@
 		"target": "es6",
 		"outDir": "out",
 		"lib": [
-			"es6"
+			"es2020",
+			"dom"
 		],
 		"sourceMap": true,
 		"rootDir": "src",
 		"strict": false
 	},
 	"exclude": [
-		"node_modules",
+		"node_modules"
 	]
 }