mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-01 17:48:36 +03:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a563a3584a | ||
|
|
758aafc570 | ||
|
|
eba90dbbf9 | ||
|
|
47647a05ae | ||
|
|
28e284f0ad | ||
|
|
c26320ec03 | ||
|
|
7d20df3b35 | ||
|
|
a84548aeac | ||
|
|
0ae80d6bcd | ||
|
|
325b17a29c | ||
|
|
0938b6384a | ||
|
|
3064849452 | ||
|
|
c416ea6789 | ||
|
|
1e22ac0d9a | ||
|
|
61e1cafdfe | ||
|
|
1844dd570b | ||
|
|
ff7f31776a | ||
|
|
fafabc3b34 | ||
|
|
d6331fee89 | ||
|
|
41b36e6e95 | ||
|
|
ece1f3118d |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
46
.vscode/launch.json
vendored
46
.vscode/launch.json
vendored
@@ -1,28 +1,24 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceRoot}/out/src/**/*.js"],
|
||||
"preLaunchTask": "npm"
|
||||
},
|
||||
{
|
||||
"name": "Launch Tests",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceRoot}/out/test"],
|
||||
"preLaunchTask": "npm"
|
||||
}
|
||||
]
|
||||
"version": "0.2.0",
|
||||
"configurations": [{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: watch",
|
||||
"env": {
|
||||
"VSCODE_DEBUG_MODE": true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -6,5 +6,6 @@
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
48
.vscode/tasks.json
vendored
48
.vscode/tasks.json
vendored
@@ -1,30 +1,20 @@
|
||||
// Available variables which can be used inside of strings.
|
||||
// ${workspaceRoot}: the root folder of the team
|
||||
// ${file}: the current opened file
|
||||
// ${fileBasename}: the current opened file's basename
|
||||
// ${fileDirname}: the current opened file's dirname
|
||||
// ${fileExtname}: the current opened file's extension
|
||||
// ${cwd}: the current working directory of the spawned process
|
||||
|
||||
// A task runner that calls a custom npm script that compiles the extension.
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "0.1.0",
|
||||
|
||||
// we want to run npm
|
||||
"command": "npm",
|
||||
|
||||
// the command is a shell script
|
||||
"isShellCommand": true,
|
||||
|
||||
// show the output window only if unrecognized errors occur.
|
||||
"showOutput": "silent",
|
||||
|
||||
// we run the custom script "compile" as defined in package.json
|
||||
"args": ["run", "compile", "--loglevel", "silent"],
|
||||
|
||||
// The tsc compiler is started in watching mode
|
||||
"isBackground": true,
|
||||
|
||||
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
|
||||
"problemMatcher": "$tsc-watch"
|
||||
}
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/test/**
|
||||
test/**
|
||||
src/**
|
||||
**/*.map
|
||||
.gitignore
|
||||
tsconfig.json
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/tslint.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2017 Geequlim
|
||||
Copyright (c) 2016-2019 The Godot Engine community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +18,4 @@ 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.
|
||||
SOFTWARE.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,36 +6,28 @@
|
||||
"name": "GDScript",
|
||||
"patterns": [
|
||||
{ "include": "#base_expression" },
|
||||
{ "include": "#self" },
|
||||
{ "include": "#logic_op" },
|
||||
{ "include": "#compare_op" },
|
||||
{ "include": "#arithmetic_op" },
|
||||
{ "include": "#assignment_op" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#self" },
|
||||
{ "include": "#const_def" },
|
||||
{ "include": "#var_def" },
|
||||
{ "include": "#type_declear"},
|
||||
{ "include": "#class_def" },
|
||||
{ "match": "\\b(?i:export|tool)\\b", "name": "storage.modifier.static.gdscript" },
|
||||
{ "include": "#builtinFuncs" },
|
||||
{
|
||||
"match": "\\b([A-Za-z_]\\w*)\\b(?=\\s*(?:[(]))",
|
||||
"name": "support.function.any-method.gdscript"
|
||||
},
|
||||
{
|
||||
"match": "(?<=[^.]\\.)\\b([A-Za-z_]\\w*)\\b(?![(])",
|
||||
"name": "variable.other.property.gdscript"
|
||||
},
|
||||
{ "include": "#classname"},
|
||||
{ "include": "#builtin_func" },
|
||||
{ "include": "#builtin_classes" },
|
||||
{ "include": "#const_vars" },
|
||||
{ "include": "#class_new"},
|
||||
{ "include": "#class_is"},
|
||||
{ "include": "#class_enum"},
|
||||
{ "include": "#function-declaration" },
|
||||
|
||||
{
|
||||
"match": "(?<=extends)\\s+[a-zA-Z_][a-zA-Z_0-9]*(\\.([a-zA-Z_][a-zA-Z_0-9]*))?",
|
||||
"name": "entity.other.inherited-class.gdscript"
|
||||
},
|
||||
|
||||
{ "include": "#builtinClasses" },
|
||||
{ "include": "#builtinProps" },
|
||||
{ "include": "#builtinConsts" },
|
||||
{ "include": "#const_vars" }
|
||||
{ "include": "#function-return-type" },
|
||||
{ "include": "#any-method" },
|
||||
{ "include": "#any-property" },
|
||||
{ "include": "#extends" },
|
||||
{ "include": "#parscal_class" }
|
||||
],
|
||||
"repository": {
|
||||
"comment": {
|
||||
@@ -50,17 +42,32 @@
|
||||
"strings": {
|
||||
"patterns": [{
|
||||
"begin": "\"",
|
||||
"end": "(?<!\\\\)\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{ "name": "constant.character.escape.untitled",
|
||||
"match": "\\."
|
||||
}
|
||||
],
|
||||
"name": "string.quoted.double.gdscript"
|
||||
},
|
||||
{
|
||||
"begin": "'",
|
||||
"end": "(?<!\\\\)'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{ "name": "constant.character.escape.untitled",
|
||||
"match": "\\."
|
||||
}
|
||||
],
|
||||
"name": "string.quoted.single.gdscript"
|
||||
},
|
||||
{
|
||||
"begin": "@\"",
|
||||
"end": "(?<!\\\\)\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{ "name": "constant.character.escape.untitled",
|
||||
"match": "\\."
|
||||
}
|
||||
],
|
||||
"name": "string.nodepath.gdscript"
|
||||
}
|
||||
]
|
||||
@@ -96,8 +103,8 @@
|
||||
},
|
||||
|
||||
"keywords": {
|
||||
"match": "\\b(?i:elif|else|for|if|while|break|continue|pass|in|is|return|onready|setget|enum|match|breakpoint|tool|extends|signal|class)\\b",
|
||||
"name": "keyword.control.gdscript"
|
||||
"match": "\\b(?i: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",
|
||||
"name": "keyword.language.gdscript"
|
||||
},
|
||||
"letter": {
|
||||
"match": "\\b(?i:true|false|null)\\b",
|
||||
@@ -130,14 +137,26 @@
|
||||
"match": "\\b(?i:(const))\\s+([a-zA-Z_][a-zA-Z_0-9]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.const.gdscript" },
|
||||
"2": { "name": "constant.other.gdscript" }
|
||||
"2": { "name": "constant.language.gdscript" }
|
||||
}
|
||||
},
|
||||
"var_def": {
|
||||
"match": "\\b(?i:(var))\\s+([a-zA-Z_][a-zA-Z_0-9]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.var.gdscript" },
|
||||
"2": { "name": "support.member.gdscript" }
|
||||
"2": { "name": "variable.language.gdscript" }
|
||||
}
|
||||
},
|
||||
"type_declear": {
|
||||
"match": "\\:\\s*([a-zA-Z_][a-zA-Z_0-9]*)",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.type.class.gdscript" }
|
||||
}
|
||||
},
|
||||
"function-return-type": {
|
||||
"match": "\\)\\s*\\-\\>\\s*([a-zA-Z_][a-zA-Z_0-9]*)\\s*\\:",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.type.class.gdscript" }
|
||||
}
|
||||
},
|
||||
"class_def": {
|
||||
@@ -147,19 +166,47 @@
|
||||
},
|
||||
"match": "(?<=^class)\\s+([a-zA-Z_]\\w*)\\s*(?=:)"
|
||||
},
|
||||
"class_new": {
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.type.class.gdscript" },
|
||||
"2": { "name": "storage.type.new.gdscript" }
|
||||
},
|
||||
"match": "\\b([a-zA-Z_][a-zA-Z_0-9]*).(new)\\("
|
||||
},
|
||||
"class_is": {
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.is.gdscript" },
|
||||
"2": { "name": "entity.name.type.class.gdscript" }
|
||||
},
|
||||
"match": "\\s+(is)\\s+([a-zA-Z_][a-zA-Z_0-9]*)"
|
||||
},
|
||||
"class_enum": {
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.type.class.gdscript" },
|
||||
"2": { "name": "constant.language.gdscript" }
|
||||
},
|
||||
"match": "\\b([A-Z][a-zA-Z_0-9]*)\\.([A-Z_0-9]+)"
|
||||
},
|
||||
"classname": {
|
||||
"match": "(?<=class_name)\\s+([a-zA-Z_][a-zA-Z_0-9]*(\\.([a-zA-Z_][a-zA-Z_0-9]*))?)",
|
||||
"name": "entity.name.type.class.gdscript"
|
||||
},
|
||||
"extends": {
|
||||
"match": "(?<=extends)\\s+[a-zA-Z_][a-zA-Z_0-9]*(\\.([a-zA-Z_][a-zA-Z_0-9]*))?",
|
||||
"name": "entity.other.inherited-class.gdscript"
|
||||
},
|
||||
"builtin_func": {
|
||||
"match": "(?<![^.]\\.|:)\\b(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)\\b(?=(\\()([^)]*)(\\)))",
|
||||
"name": "support.function.builtin.gdscript"
|
||||
},
|
||||
"builtinClasses": {
|
||||
"match": "(?<![^.]\\.|:)\\b(Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D)\\b",
|
||||
"builtin_classes": {
|
||||
"match": "(?<![^.]\\.|:)\\b(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",
|
||||
"name": "support.class.library.gdscript"
|
||||
},
|
||||
"const_vars": {
|
||||
"match": "\\b([A-Z_0-9]+)\\b",
|
||||
"name": "constant.other.gdscript"
|
||||
"name": "constant.language.gdscript"
|
||||
},
|
||||
|
||||
"function-declaration": {
|
||||
"name": "meta.function.gdscript",
|
||||
"begin": "(?x)\n \\s*\n (?:\\b(static) \\s+)? \\b(func|signal)\\s+\n (?=\n [[:alpha:]_][[:word:]]* \\s* \\(\n )\n",
|
||||
@@ -213,6 +260,14 @@
|
||||
{ "include": "#annotated-parameter" }
|
||||
]
|
||||
},
|
||||
"any-method": {
|
||||
"match": "\\b([A-Za-z_]\\w*)\\b(?=\\s*(?:[(]))",
|
||||
"name": "support.function.any-method.gdscript"
|
||||
},
|
||||
"any-property": {
|
||||
"match": "(?<=[^.]\\.)\\b([A-Za-z_]\\w*)\\b(?![(])",
|
||||
"name": "variable.other.property.gdscript"
|
||||
},
|
||||
"parameter-special": {
|
||||
"match": "(?x)\n \\b ((self)|(cls)) \\b \\s*(?:(,)|(?=\\)))\n",
|
||||
"captures": {
|
||||
@@ -269,6 +324,12 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"parscal_class": {
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.type.class.gdscript" }
|
||||
},
|
||||
"match": "([A-Z][a-zA-Z_0-9]*)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,12 @@
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^\\s*((class|func|else|elif|for|if|match|while|enum)|(.*\\sdo\\b))\\b[^\\{;]*$",
|
||||
"decreaseIndentPattern": "^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(else|elif)\\b)"
|
||||
}
|
||||
},
|
||||
"folding": {
|
||||
"offSide": true,
|
||||
"markers": {
|
||||
"start": "^\\s*#\\s*region\\b",
|
||||
"end": "^\\s*#\\s*endregion\\b"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
|
||||
// Place your snippets for JavaScript React here. Each snippet is defined under a snippet name and has a prefix, body and
|
||||
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, ${id} and ${id:label} and ${1:label} for variables. Variables with the same id are connected.
|
||||
// Example:
|
||||
"Inner class": {
|
||||
"prefix": "class",
|
||||
"body": [
|
||||
|
||||
2
doc/.gitignore
vendored
2
doc/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
classes/*.xml
|
||||
classes-*.xml
|
||||
83763
doc/classes-2.1.json
83763
doc/classes-2.1.json
File diff suppressed because it is too large
Load Diff
90257
doc/classes-3.0.json
90257
doc/classes-3.0.json
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
|
||||
def parseClass(data):
|
||||
dictCls = dict(data.attrib)
|
||||
dictCls['brief_description'] = data.find("brief_description").text.strip()
|
||||
dictCls['description'] = data.find("description").text.strip()
|
||||
dictCls['methods'] = []
|
||||
for m in data.find("methods"):
|
||||
dictCls['methods'].append(parseMethod(m))
|
||||
dictCls['signals'] = []
|
||||
for s in (data.find("signals") if data.find("signals") is not None else []):
|
||||
dictCls['signals'].append(parseMethod(s))
|
||||
dictCls['constants'] = []
|
||||
for c in (data.find("constants") if data.find("constants") is not None else []):
|
||||
dictCls['constants'].append(parseConstant(c))
|
||||
dictCls['properties'] = []
|
||||
for m in (data.find("members") if data.find("members") is not None else []):
|
||||
dictCls['properties'].append(parseProperty(m))
|
||||
dictCls['theme_properties'] = []
|
||||
for thi in (data.find("theme_items") if data.find("theme_items") is not None else []):
|
||||
dictCls['theme_properties'].append(parseProperty(thi))
|
||||
return dictCls
|
||||
|
||||
def parseMethod(data):
|
||||
dictMethod = dict(data.attrib)
|
||||
dictMethod['description'] = data.find("description").text.strip()
|
||||
dictMethod['return_type'] = data.find("return").attrib["type"] if data.find("return") is not None else ""
|
||||
if "qualifiers" not in dictMethod: dictMethod["qualifiers"] = ""
|
||||
dictMethod["arguments"] = []
|
||||
for arg in data.iter('argument'):
|
||||
dictMethod["arguments"].append(parseArgument(arg))
|
||||
return dictMethod
|
||||
|
||||
def parseArgument(data):
|
||||
dictArg = dict(data.attrib)
|
||||
if "dictArg" in dictArg: dictArg.pop("index")
|
||||
dictArg["default_value"] = dictArg["default"] if "default" in dictArg else ""
|
||||
if "default" in dictArg: dictArg.pop("default")
|
||||
return dictArg
|
||||
|
||||
def parseConstant(data):
|
||||
dictConst = dict(data.attrib)
|
||||
dictConst["description"] = data.text.strip()
|
||||
return dictConst
|
||||
|
||||
def parseProperty(data):
|
||||
dictProp = dict(data.attrib)
|
||||
dictProp["description"] = data.text.strip()
|
||||
return dictProp
|
||||
|
||||
def main():
|
||||
if len(sys.argv) >=2 :
|
||||
tree = ET.parse(open(sys.argv[1], 'r'))
|
||||
classes = {}
|
||||
for cls in tree.getroot():
|
||||
dictCls = parseClass(cls)
|
||||
classes[dictCls['name']] = dictCls
|
||||
jsonContent = json.dumps({"classes": classes, "version": "2.1.4"}, ensure_ascii=False, indent=2)
|
||||
print(jsonContent)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
import os
|
||||
|
||||
def glob_path(path, pattern):
|
||||
import os, fnmatch
|
||||
result = []
|
||||
for root, _, files in os.walk(path):
|
||||
for filename in files:
|
||||
if fnmatch.fnmatch(filename, pattern):
|
||||
result.append(os.path.join(root, filename))
|
||||
return result
|
||||
|
||||
def parseClass(data):
|
||||
dictCls = dict(data.attrib)
|
||||
dictCls['brief_description'] = data.find("brief_description").text.strip()
|
||||
dictCls['description'] = data.find("description").text.strip()
|
||||
dictCls['methods'] = []
|
||||
for m in data.find("methods"):
|
||||
dictCls['methods'].append(parseMethod(m))
|
||||
dictCls['signals'] = []
|
||||
for s in (data.find("signals") if data.find("signals") is not None else []):
|
||||
dictCls['signals'].append(parseMethod(s))
|
||||
dictCls['constants'] = []
|
||||
for c in (data.find("constants") if data.find("constants") is not None else []):
|
||||
dictCls['constants'].append(parseConstant(c))
|
||||
dictCls['properties'] = []
|
||||
for m in (data.find("members") if data.find("members") is not None else []):
|
||||
dictCls['properties'].append(parseProperty(m))
|
||||
dictCls['theme_properties'] = []
|
||||
for thi in (data.find("theme_items") if data.find("theme_items") is not None else []):
|
||||
dictCls['theme_properties'].append(parseProperty(thi))
|
||||
return dictCls
|
||||
|
||||
def parseMethod(data):
|
||||
dictMethod = dict(data.attrib)
|
||||
dictMethod['description'] = data.find("description").text.strip()
|
||||
dictMethod['return_type'] = data.find("return").attrib["type"] if data.find("return") is not None else ""
|
||||
if "qualifiers" not in dictMethod: dictMethod["qualifiers"] = ""
|
||||
dictMethod["arguments"] = []
|
||||
for arg in data.iter('argument'):
|
||||
dictMethod["arguments"].append(parseArgument(arg))
|
||||
return dictMethod
|
||||
|
||||
def parseArgument(data):
|
||||
dictArg = dict(data.attrib)
|
||||
if "dictArg" in dictArg: dictArg.pop("index")
|
||||
dictArg["default_value"] = dictArg["default"] if "default" in dictArg else ""
|
||||
if "default" in dictArg: dictArg.pop("default")
|
||||
return dictArg
|
||||
|
||||
def parseConstant(data):
|
||||
dictConst = dict(data.attrib)
|
||||
dictConst["description"] = data.text.strip()
|
||||
return dictConst
|
||||
|
||||
def parseProperty(data):
|
||||
dictProp = dict(data.attrib)
|
||||
dictProp["description"] = data.text.strip()
|
||||
return dictProp
|
||||
|
||||
def main():
|
||||
if len(sys.argv) >=2 :
|
||||
if os.path.isdir(sys.argv[1]):
|
||||
classes = {}
|
||||
for f in glob_path(sys.argv[1], "**.xml"):
|
||||
if f.find("/classes/") == -1 and f.find("/doc_classes/") == -1:
|
||||
continue
|
||||
tree = ET.parse(open(f, 'r'))
|
||||
cls = tree.getroot()
|
||||
dictCls = parseClass(cls)
|
||||
classes[dictCls['name']] = dictCls
|
||||
jsonContent = json.dumps({"classes": classes, "version": "3.0.4"}, ensure_ascii=False, indent=2)
|
||||
print(jsonContent)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
2236
package-lock.json
generated
2236
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
268
package.json
268
package.json
@@ -1,157 +1,115 @@
|
||||
{
|
||||
"name": "godot-tools",
|
||||
"displayName": "Godot Tools",
|
||||
"icon": "icon.png",
|
||||
"description": "Tools for game development with godot game engine",
|
||||
"version": "0.3.7",
|
||||
"publisher": "geequlim",
|
||||
"repository": "https://github.com/GodotExplorer/godot-tools",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.1.21"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:engine.cfg",
|
||||
"workspaceContains:project.godot",
|
||||
"onLanguage:gdscript"
|
||||
],
|
||||
"main": "./out/src/extension",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "godot.updateWorkspaceSymbols",
|
||||
"title": "GodotTools: Update workspace symbols"
|
||||
},
|
||||
{
|
||||
"command": "godot.runWorkspace",
|
||||
"title": "GodotTools: Run workspace as Godot project"
|
||||
},
|
||||
{
|
||||
"command": "godot.openWithEditor",
|
||||
"title": "GodotTools: Open workspace with Godot editor"
|
||||
},
|
||||
{
|
||||
"command": "godot.runCurrentScene",
|
||||
"title": "GodotTools: Run current scene"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Godot Tools configuration",
|
||||
"properties": {
|
||||
"GodotTools.maxNumberOfProblems": {
|
||||
"type": "number",
|
||||
"default": 100,
|
||||
"description": "Controls the maximum number of problems produced by the server."
|
||||
},
|
||||
"GodotTools.editorPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The absolute path to the Godot executable"
|
||||
},
|
||||
"GodotTools.workspaceDocumentWithMarkdown": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Render workspace documentations as Markdown content"
|
||||
},
|
||||
"GodotTools.ignoreIndentedVars": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Only parse variables without indents in GDScript"
|
||||
},
|
||||
"GodotTools.godotVersion": {
|
||||
"type": "number",
|
||||
"default": 3.0,
|
||||
"description": "The Godot version of your project"
|
||||
},
|
||||
"GodotTools.parseTextScene": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Parse a file as a Godot scene when the file name ends with tscn"
|
||||
},
|
||||
"GodotTools.completeNodePath": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Show node paths within a workspace as part of code completion"
|
||||
},
|
||||
"GodotTools.godotProjectRoot": {
|
||||
"type": "string",
|
||||
"default": "${workspaceRoot}",
|
||||
"description": "Your Godot project's directory"
|
||||
},
|
||||
"GodotTools.enableSyntaxChecking": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Turn on/off syntax checking for GDScript"
|
||||
},
|
||||
"GodotTools.lint": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"semicolon": false,
|
||||
"conditionBrackets": false,
|
||||
"unusedSymbols": true
|
||||
},
|
||||
"description": "Lint configuration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "gdscript",
|
||||
"aliases": [
|
||||
"GDScript",
|
||||
"gdscript"
|
||||
],
|
||||
"extensions": [
|
||||
".gd"
|
||||
],
|
||||
"configuration": "./configurations/gdscript-configuration.json"
|
||||
},
|
||||
{
|
||||
"id": "properties",
|
||||
"extensions": [
|
||||
"cfg",
|
||||
"tres",
|
||||
"tscn",
|
||||
"godot",
|
||||
"gdns",
|
||||
"gdnlib"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "gdscript",
|
||||
"scopeName": "source.gdscript",
|
||||
"path": "./configurations/GDScript.tmLanguage.json"
|
||||
}
|
||||
],
|
||||
"snippets": [
|
||||
{
|
||||
"language": "gdscript",
|
||||
"path": "./configurations/snippets.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "tsc -p ./",
|
||||
"compile": "node ./node_modules/typescript/bin/tsc -p ./",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.9.4",
|
||||
"mocha": "^5.2.0",
|
||||
"typescript": "^3.0.3",
|
||||
"vscode": "^1.1.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": "^7.1.1",
|
||||
"vscode-debugprotocol": "^1.17.0",
|
||||
"vscode-debugadapter": "^1.17.0"
|
||||
}
|
||||
"name": "godot-tools",
|
||||
"displayName": "godot-tools",
|
||||
"icon": "icon.png",
|
||||
"version": "0.9.1",
|
||||
"description": "Tools for game development with godot game engine",
|
||||
"repository": "https://github.com/godotengine/godot-vscode-plugin",
|
||||
"author": "The Godot Engine community",
|
||||
"publisher": "geequlim",
|
||||
"engines": {
|
||||
"vscode": "^1.33.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:project.godot",
|
||||
"onLanguage:gdscript"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"scripts": {
|
||||
"vscode:prepublish": "yarn run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"test": "yarn run compile && node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "godot-tool.open_editor",
|
||||
"title": "Godot Tools: Open workspace with Godot editor"
|
||||
},
|
||||
{
|
||||
"command": "godot-tool.run_project",
|
||||
"title": "Godot Tools: Run workspace as Godot project"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Godot Tools configuration",
|
||||
"properties": {
|
||||
"godot_tools.gdscript_lsp_server_port": {
|
||||
"type": "number",
|
||||
"default": 6008,
|
||||
"description": "The websocket server port of the GDScript language server"
|
||||
},
|
||||
"godot_tools.editor_path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The absolute path to the Godot editor executable"
|
||||
},
|
||||
"godot-tool.check_status": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The absolute path to the Godot editor executable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "gdscript",
|
||||
"aliases": [
|
||||
"GDScript",
|
||||
"gdscript"
|
||||
],
|
||||
"extensions": [
|
||||
".gd"
|
||||
],
|
||||
"configuration": "./configurations/gdscript-configuration.json"
|
||||
},
|
||||
{
|
||||
"id": "properties",
|
||||
"extensions": [
|
||||
"cfg",
|
||||
"tres",
|
||||
"tscn",
|
||||
"godot",
|
||||
"gdns",
|
||||
"gdnlib"
|
||||
]
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "gdscript",
|
||||
"scopeName": "source.gdscript",
|
||||
"path": "./configurations/GDScript.tmLanguage.json"
|
||||
}
|
||||
],
|
||||
"snippets": [
|
||||
{
|
||||
"language": "gdscript",
|
||||
"path": "./configurations/snippets.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"typescript": "^3.4.5",
|
||||
"vscode": "^1.1.33"
|
||||
},
|
||||
"dependencies": {
|
||||
"global": "^4.4.0",
|
||||
"marked": "^0.7.0",
|
||||
"prismjs": "^1.17.1",
|
||||
"vscode-languageclient": "^5.2.1",
|
||||
"ws": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
331
src/config.ts
331
src/config.ts
@@ -1,331 +0,0 @@
|
||||
import GDScriptSymbolParser from './gdscript/symbolparser';
|
||||
import * as fs from 'fs';
|
||||
import {CompletionItem, CompletionItemKind, TextEdit, Range, workspace} from 'vscode';
|
||||
|
||||
interface NodeInfo {
|
||||
name: string,
|
||||
type: string,
|
||||
parent: string,
|
||||
instance: string
|
||||
};
|
||||
|
||||
interface CompletionSymbols {
|
||||
classes : CompletionItem[],
|
||||
functions : CompletionItem[],
|
||||
signals : CompletionItem[],
|
||||
constants : CompletionItem[],
|
||||
properties : CompletionItem[],
|
||||
nodes : CompletionItem[],
|
||||
builtinConstants: CompletionItem[]
|
||||
};
|
||||
|
||||
class Config {
|
||||
|
||||
private workspaceSymbols; // filePath: GDScript in symbolparser.ts
|
||||
private builtinCompletions : CompletionSymbols;
|
||||
private builtinClassDoc;
|
||||
public parser: GDScriptSymbolParser;
|
||||
// scriptpath : scenepath
|
||||
public scriptSceneMap: Object;
|
||||
// scenepath : NodeInfo[]
|
||||
public nodeInfoMap: Object;
|
||||
// symbolname: {completionItem: CompletionItem, rowDoc: docdata}
|
||||
public builtinSymbolInfoMap: Object;
|
||||
|
||||
constructor() {
|
||||
this.builtinCompletions = {
|
||||
classes : [],
|
||||
functions : [],
|
||||
signals : [],
|
||||
constants : [],
|
||||
properties : [],
|
||||
nodes : [],
|
||||
builtinConstants: []
|
||||
};
|
||||
this.workspaceSymbols = {};
|
||||
this.builtinSymbolInfoMap = {};
|
||||
this.nodeInfoMap = {};
|
||||
this.scriptSceneMap = {};
|
||||
this.parser = new GDScriptSymbolParser();
|
||||
}
|
||||
|
||||
loadSymbolsFromFile(path) {
|
||||
var ignoreIndentedVars = false;
|
||||
if(workspace)
|
||||
ignoreIndentedVars = workspace.getConfiguration("GodotTools").get("ignoreIndentedVars", false);
|
||||
const script = this.parser.parseFile(path, ignoreIndentedVars);
|
||||
this.setSymbols(path, script);
|
||||
return script;
|
||||
}
|
||||
|
||||
setSymbols(path, s) {
|
||||
this.workspaceSymbols[this.normalizePath(path)] = s;
|
||||
}
|
||||
|
||||
getSymbols(path) {
|
||||
return this.workspaceSymbols[this.normalizePath(path)];
|
||||
}
|
||||
|
||||
setAllSymbols(s) {
|
||||
this.workspaceSymbols = s;
|
||||
}
|
||||
|
||||
getAllSymbols() {
|
||||
return this.workspaceSymbols;
|
||||
}
|
||||
|
||||
normalizePath(path) {
|
||||
let newpath = path;
|
||||
if( path.indexOf(":") != -1){
|
||||
let parts = path.split(":");
|
||||
newpath = parts[0].toUpperCase();
|
||||
newpath += ":";
|
||||
for(let i=1; i<parts.length; i++)
|
||||
newpath += parts[i];
|
||||
}
|
||||
newpath = newpath.replace(/\\/g, "/");
|
||||
return newpath;
|
||||
}
|
||||
|
||||
loadClasses(docfile: string): boolean {
|
||||
let done: boolean = false;
|
||||
try {
|
||||
if(fs.existsSync(docfile) && fs.statSync(docfile).isFile()) {
|
||||
const content = fs.readFileSync(docfile, "utf-8");
|
||||
const docdata = JSON.parse(content);
|
||||
if(docdata.classes) {
|
||||
this.builtinClassDoc = docdata.classes;
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
if(done) {
|
||||
for (let key of Object.keys(this.builtinClassDoc)) {
|
||||
const classdoc = this.builtinClassDoc[key];
|
||||
const builtinSymbolInfoMap = this.builtinSymbolInfoMap;
|
||||
// ---------------------- class -----------------
|
||||
const item: CompletionItem = new CompletionItem(classdoc.name, CompletionItemKind.Class);
|
||||
item.detail = 'Native Class';
|
||||
item.documentation = classdoc.brief_description + " \n" +classdoc.description;
|
||||
this.builtinCompletions.classes.push(item);
|
||||
builtinSymbolInfoMap[classdoc.name] = {completionItem: item, rowDoc: classdoc};
|
||||
// ----------------------- functions -----------------------
|
||||
const parsMethod = (m, kind: CompletionItemKind, insertAction=(name)=>name)=>{
|
||||
const mi = new CompletionItem(m.name, kind);
|
||||
mi.insertText = insertAction(m.name) + (m.arguments.length==0?"()":"");
|
||||
mi.filterText = m.name
|
||||
mi.sortText = m.name
|
||||
mi.detail = m.return_type;
|
||||
let argstr = "";
|
||||
m.arguments.map(arg=>{
|
||||
argstr += `${arg.type} ${arg.name}${arg.default_value.length>0?'='+arg.default_value:''}${m.arguments.indexOf(arg)==m.arguments.length-1?'':', '}`;
|
||||
});
|
||||
// mi.label=`${m.name}(${argstr}) ${m.qualifiers}`;
|
||||
let methodName = `${classdoc.name}.${m.name}`;
|
||||
if (classdoc.name == m.name) methodName = m.name;
|
||||
let mdoc = `${m.return_type} ${methodName}(${argstr}) ${m.qualifiers}`;
|
||||
mdoc += " \n\n";
|
||||
mdoc += m.description;
|
||||
mi.documentation = mdoc;
|
||||
if(CompletionItemKind.Interface == kind)
|
||||
this.builtinCompletions.signals.push(mi);
|
||||
else
|
||||
this.builtinCompletions.functions.push(mi);
|
||||
builtinSymbolInfoMap[`${classdoc.name}.${m.name}`] = {completionItem: mi, rowDoc: m};
|
||||
};
|
||||
// methods
|
||||
const methods = classdoc.methods
|
||||
methods.map(m=>parsMethod(m, CompletionItemKind.Method));
|
||||
// signals
|
||||
const signals = classdoc.signals;
|
||||
signals.map(s=>parsMethod(s, CompletionItemKind.Interface));
|
||||
// ------------------------------ constants ---------------------
|
||||
const constants = classdoc.constants;
|
||||
constants.map(c=>{
|
||||
const ci = new CompletionItem(c.name, CompletionItemKind.Enum);
|
||||
ci.detail = c.value;
|
||||
ci.documentation = `${classdoc.name}.${c.name} = ${c.value}`;
|
||||
if(key[0] == "@" || key == "Node" || key == "Control")
|
||||
this.builtinCompletions.builtinConstants.push(ci);
|
||||
else
|
||||
this.builtinCompletions.constants.push(ci);
|
||||
builtinSymbolInfoMap[`${classdoc.name}.${c.name}`] = {completionItem: ci, rowDoc: c};
|
||||
});
|
||||
// ----------------------- properties -----------------------
|
||||
const parseProp = (p) => {
|
||||
const pi = new CompletionItem(p.name, CompletionItemKind.Property);
|
||||
pi.detail = `${p.type} of ${classdoc.name}`;
|
||||
pi.documentation = p.description;
|
||||
this
|
||||
.builtinCompletions
|
||||
.properties
|
||||
.push(pi);
|
||||
builtinSymbolInfoMap[`${classdoc.name}.${p.name}`] = {
|
||||
completionItem: pi,
|
||||
rowDoc: p
|
||||
};
|
||||
};
|
||||
// properties
|
||||
const properties = classdoc.properties;
|
||||
properties.map(p=>parseProp(p));
|
||||
// theme_properties
|
||||
const theme_properties = classdoc.theme_properties;
|
||||
theme_properties.map(p=>parseProp(p));
|
||||
}
|
||||
}
|
||||
return done;
|
||||
};
|
||||
|
||||
getWorkspaceCompletionItems(script_files = []) : CompletionSymbols {
|
||||
const symbols = {
|
||||
classes: [],
|
||||
functions: [],
|
||||
signals: [],
|
||||
constants: [],
|
||||
properties: [],
|
||||
nodes: [],
|
||||
builtinConstants: []
|
||||
};
|
||||
if (script_files.length == 0)
|
||||
script_files = Object.keys(this.workspaceSymbols);
|
||||
for (let path of script_files) {
|
||||
const script = this.workspaceSymbols[path];
|
||||
if (workspace) {
|
||||
const root = this.normalizePath(workspace.rootPath) + "/";
|
||||
if (path.startsWith(root))
|
||||
path = path.replace(root, "");
|
||||
}
|
||||
const addScriptItems = (items, kind: CompletionItemKind, kindName:string = "Symbol", insertText = (n)=>n)=>{
|
||||
const _items: CompletionItem[] = [];
|
||||
for (let name of Object.keys(items)) {
|
||||
const signature = (script.signatures && script.signatures[name])?script.signatures[name]:"";
|
||||
const cvalue = (script.constvalues && script.constvalues[name])?script.constvalues[name]:"";
|
||||
const item = new CompletionItem(name+signature, kind);
|
||||
item.sortText = name;
|
||||
item.filterText = name;
|
||||
item.detail = cvalue;
|
||||
item.insertText = insertText(name) + (signature=="()"?"()":"");
|
||||
item.documentation = (script.documents && script.documents[name])?script.documents[name]+"\r\n":"";
|
||||
item.documentation += `${kindName} defined in ${path}`;
|
||||
_items.push(item);
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
|
||||
symbols.classes = [ ...(symbols.classes), ...(addScriptItems(script.classes, CompletionItemKind.Class, "Class"))]
|
||||
symbols.functions = [ ...(symbols.functions), ...(addScriptItems(script.functions, CompletionItemKind.Method, "Method"))]
|
||||
symbols.signals = [ ...(symbols.signals), ...(addScriptItems(script.signals, CompletionItemKind.Interface, "Signal"))]
|
||||
symbols.properties = [ ...(symbols.properties), ...(addScriptItems(script.variables, CompletionItemKind.Variable, "Variable"))]
|
||||
symbols.constants = [ ...(symbols.constants), ...(addScriptItems(script.constants, CompletionItemKind.Enum, "Constant"))]
|
||||
|
||||
if(script.enumerations)
|
||||
symbols.constants = [...(symbols.constants), ...(addScriptItems(script.enumerations, CompletionItemKind.Enum, "Enumeration"))];
|
||||
}
|
||||
|
||||
if(workspace.getConfiguration("GodotTools").get("completeNodePath", false)) {
|
||||
const addSceneNodes = ()=>{
|
||||
const _items: CompletionItem[] = [];
|
||||
for (let scnenepath of Object.keys(this.nodeInfoMap)) {
|
||||
const nodes: NodeInfo[] = this.nodeInfoMap[scnenepath];
|
||||
nodes.map((n=>{
|
||||
const item = new CompletionItem(n.name, CompletionItemKind.Reference);
|
||||
item.detail = n.type;
|
||||
item.documentation = `${n.parent}/${n.name} in ${scnenepath}`;
|
||||
_items.push(item);
|
||||
|
||||
const fullitem = new CompletionItem(`${n.parent}/${n.name}`, CompletionItemKind.Reference);
|
||||
fullitem.detail = n.type;
|
||||
fullitem.filterText = n.name;
|
||||
fullitem.sortText = n.name;
|
||||
fullitem.documentation = `${n.parent}/${n.name} in ${scnenepath}`;
|
||||
_items.push(fullitem);
|
||||
}));
|
||||
}
|
||||
return _items;
|
||||
};
|
||||
symbols.nodes = [...(symbols.nodes), ...(addSceneNodes())];
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
loadScene(scenePath: string) {
|
||||
if(fs.existsSync(scenePath) && fs.statSync(scenePath).isFile()) {
|
||||
try {
|
||||
const content: string = fs.readFileSync(scenePath, 'utf-8');
|
||||
if(content) {
|
||||
// extern resources
|
||||
const exteres = {};
|
||||
let reg = /ext_resource path="res:\/\/(.*)" type="(.*)" id=(\d+)/g;
|
||||
let match = reg.exec(content);
|
||||
while (match != null) {
|
||||
const path = match[1];
|
||||
const type = match[2];
|
||||
const id = match[3];
|
||||
exteres[id] = {path, type};
|
||||
if (type == "Script") {
|
||||
let workspacescenepath = scenePath;
|
||||
if(workspace)
|
||||
workspacescenepath = workspace.asRelativePath(scenePath);
|
||||
this.scriptSceneMap[path] = workspacescenepath;
|
||||
}
|
||||
match = reg.exec(content);
|
||||
}
|
||||
// nodes
|
||||
const nodes: NodeInfo[] = [];
|
||||
reg = /node\s+name="(.*)"\s+type="(.*)"\s+parent="(.*)"/g;
|
||||
match = reg.exec(content);
|
||||
while (match != null) {
|
||||
nodes.push({
|
||||
name : match[1],
|
||||
type : match[2],
|
||||
parent : match[3],
|
||||
instance: ""
|
||||
});
|
||||
match = reg.exec(content);
|
||||
}
|
||||
// packed scenes
|
||||
reg = /node name="(.*)" parent="(.*)" instance=ExtResource\(\s*(\d+)\s*\)/g;
|
||||
match = reg.exec(content);
|
||||
while (match != null) {
|
||||
const id = match[3];
|
||||
nodes.push({
|
||||
name : match[1],
|
||||
type : exteres[id].type,
|
||||
parent : match[2],
|
||||
instance: exteres[id].path
|
||||
});
|
||||
match = reg.exec(content);
|
||||
}
|
||||
if(workspace)
|
||||
scenePath = workspace.asRelativePath(scenePath);
|
||||
this.nodeInfoMap[scenePath] = nodes;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClass(name: string) {
|
||||
return this.builtinClassDoc[name];
|
||||
}
|
||||
|
||||
getBuiltinCompletions() {
|
||||
return this.builtinCompletions;
|
||||
}
|
||||
|
||||
getBuiltinClassNameList() {
|
||||
let namelist = null;
|
||||
if (this.builtinClassDoc)
|
||||
namelist = Object.keys(this.builtinClassDoc);
|
||||
if(!namelist)
|
||||
namelist = [];
|
||||
return namelist;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default new Config();
|
||||
@@ -1,162 +0,0 @@
|
||||
// import {
|
||||
// DebugSession,
|
||||
// InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, OutputEvent, Event,
|
||||
// Thread, StackFrame, Scope, Source, Handles, Breakpoint
|
||||
// } from 'vscode-debugadapter';
|
||||
// import {DebugProtocol} from 'vscode-debugprotocol';
|
||||
// import * as fs from 'fs';
|
||||
// import * as path from 'path';
|
||||
// const cmd = require('node-cmd');
|
||||
|
||||
// export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
|
||||
// godot: string;
|
||||
// projectDir: string;
|
||||
// runWithEditor: boolean;
|
||||
// params: string[];
|
||||
// }
|
||||
|
||||
// class GodotDebugSession extends DebugSession {
|
||||
|
||||
// // we don't support multiple threads, so we can use a hardcoded ID for the default thread
|
||||
// private static THREAD_ID = 1;
|
||||
|
||||
// /**
|
||||
// * Creates a new debug adapter that is used for one debug session.
|
||||
// * We configure the default implementation of a debug adapter here.
|
||||
// */
|
||||
// public constructor() {
|
||||
// super();
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * The 'initialize' request is the first request called by the frontend
|
||||
// * to interrogate the features the debug adapter provides.
|
||||
// */
|
||||
// protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
|
||||
|
||||
// // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time,
|
||||
// // we request them early by sending an 'initializeRequest' to the frontend.
|
||||
// // The frontend will end the configuration sequence by calling 'configurationDone' request.
|
||||
// this.sendEvent(new InitializedEvent());
|
||||
|
||||
// // This debug adapter implements the configurationDoneRequest.
|
||||
// response.body.supportsConfigurationDoneRequest = true;
|
||||
|
||||
// // make VS Code to use 'evaluate' when hovering over source
|
||||
// response.body.supportsEvaluateForHovers = true;
|
||||
|
||||
// // make VS Code to show a 'step back' button
|
||||
// response.body.supportsStepBack = true;
|
||||
|
||||
// this.log("initializeRequest");
|
||||
// this.log_err("initializeRequest");
|
||||
// this.log_console("initializeRequest");
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
|
||||
// for(let key of Object.keys(args))
|
||||
// this.log(`${key} : ${args[key]}`);
|
||||
// let workspaceValid = false
|
||||
// if(args.godot && fs.existsSync(args.godot) && fs.statSync(args.godot).isFile() ) {
|
||||
// if(args.projectDir && fs.existsSync(args.projectDir) && fs.statSync(args.projectDir).isDirectory() ) {
|
||||
// let cfg = path.join(args.projectDir, "engine.cfg");
|
||||
// if( fs.existsSync(cfg) && fs.statSync(cfg).isFile())
|
||||
// workspaceValid = true;
|
||||
// }
|
||||
// }
|
||||
// if(workspaceValid) {
|
||||
// let params = `-path ${args.projectDir} `;
|
||||
// if(args.runWithEditor)
|
||||
// params += "-e";
|
||||
// if(args.params) {
|
||||
// for(let p of args.params)
|
||||
// params += " " + p;
|
||||
// }
|
||||
// let cmdcontent = `${args.godot} ${params}`;
|
||||
// this.log(cmdcontent)
|
||||
// // TODO: print outputs in terminal console
|
||||
// cmd.run(cmdcontent);
|
||||
// this.sendEvent(new TerminatedEvent());
|
||||
// }
|
||||
// else {
|
||||
// this.log_err("Invalidate path of projectDir or godot:");
|
||||
// this.log_err(JSON.stringify(args, null, '\t'));
|
||||
// this.sendEvent(new TerminatedEvent());
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
|
||||
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
|
||||
|
||||
// // return the default thread
|
||||
// response.body = {
|
||||
// threads: [
|
||||
// new Thread(GodotDebugSession.THREAD_ID, "thread 1")
|
||||
// ]
|
||||
// };
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line.
|
||||
// */
|
||||
// protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
|
||||
// this.sendEvent(new TerminatedEvent());
|
||||
// }
|
||||
|
||||
// protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments) : void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void {
|
||||
// this.sendResponse(response);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Fire StoppedEvent if line is not empty.
|
||||
// */
|
||||
// private fireStepEvent(response: DebugProtocol.Response, ln: number): boolean {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// private log(msg: string) {
|
||||
// const e = new OutputEvent(msg, "stdout");
|
||||
// this.sendEvent(e);
|
||||
// }
|
||||
// private log_err(msg: string) {
|
||||
// const e = new OutputEvent(msg, "stderr");
|
||||
// this.sendEvent(e);
|
||||
// }
|
||||
// private log_console(msg: string) {
|
||||
// const e = new OutputEvent(msg, "console");
|
||||
// this.sendEvent(e);
|
||||
// }
|
||||
// }
|
||||
|
||||
// DebugSession.run(GodotDebugSession);
|
||||
@@ -1,13 +1,17 @@
|
||||
'use strict';
|
||||
import { workspace, Disposable, ExtensionContext } from 'vscode';
|
||||
import WindowWatch from "./window_watcher";
|
||||
import ToolManager from './tool_manager';
|
||||
|
||||
let tool: ToolManager = null;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
tool = new ToolManager(context);
|
||||
context.subscriptions.push(tool);
|
||||
context.subscriptions.push(new WindowWatch());
|
||||
console.log("[GodotTools]: Extension Activated");
|
||||
}
|
||||
import { ExtensionContext } from "vscode";
|
||||
import { GodotTools } from "./godot-tools";
|
||||
|
||||
|
||||
let tools: GodotTools = null;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
tools = new GodotTools(context);
|
||||
tools.activate();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tools.deactivate();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import {
|
||||
CompletionItemProvider,
|
||||
Position,
|
||||
TextDocument,
|
||||
CancellationToken,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
languages,
|
||||
Disposable,
|
||||
CompletionItemKind
|
||||
} from 'vscode';
|
||||
|
||||
import requestGodot from '../request';
|
||||
import config from '../config';
|
||||
|
||||
interface CompleteRequest {
|
||||
path: string,
|
||||
text: string,
|
||||
cursor: {
|
||||
row: number,
|
||||
column: number
|
||||
}
|
||||
}
|
||||
|
||||
interface CompletionResult {
|
||||
suggestions: string[],
|
||||
hint: string,
|
||||
prefix: string,
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
class GDScriptCompletionItemProvider implements CompletionItemProvider {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
private get_previous_flag(document : TextDocument, position : Position): string {
|
||||
const line = document.lineAt(position).text;
|
||||
let res = "";
|
||||
for (let index = position.character; index >= 0; index--) {
|
||||
res = line[index];
|
||||
if (['.', '$', '"', "'"].indexOf(res) != -1 )
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
provideCompletionItems(document : TextDocument, position : Position, token : CancellationToken) : CompletionItem[] | Thenable < CompletionItem[] > | CompletionList | Thenable < CompletionList > {
|
||||
|
||||
const lastFlag = this.get_previous_flag(document, position);
|
||||
const builtins = config.getBuiltinCompletions();
|
||||
|
||||
let items:CompletionItem[] = [...(builtins.builtinConstants)];
|
||||
if(!lastFlag || lastFlag.trim().length == 0) {
|
||||
const workspaces = config.getWorkspaceCompletionItems([config.normalizePath(document.fileName)]);
|
||||
items = [
|
||||
...items,
|
||||
...(workspaces.functions),
|
||||
...(workspaces.classes),
|
||||
...(workspaces.constants),
|
||||
...(workspaces.properties),
|
||||
...(builtins.functions),
|
||||
...(builtins.classes),
|
||||
...(builtins.constants),
|
||||
]
|
||||
}
|
||||
else {
|
||||
const workspaces = config.getWorkspaceCompletionItems();
|
||||
if(lastFlag.trim() == ".") {
|
||||
items = [
|
||||
...items,
|
||||
...(workspaces.functions),
|
||||
...(workspaces.constants),
|
||||
...(workspaces.properties),
|
||||
...(workspaces.classes),
|
||||
...(builtins.functions),
|
||||
...(builtins.constants),
|
||||
...(builtins.properties)
|
||||
]
|
||||
}
|
||||
else if(lastFlag.trim() == "'" || lastFlag.trim() == '"') {
|
||||
items = [
|
||||
...items,
|
||||
...(workspaces.signals),
|
||||
...(workspaces.functions),
|
||||
...(workspaces.properties),
|
||||
...(builtins.signals),
|
||||
...(builtins.functions),
|
||||
...(builtins.properties),
|
||||
...(workspaces.nodes),
|
||||
]
|
||||
}
|
||||
else if(lastFlag.trim() == "$") {
|
||||
items = [ ...(workspaces.nodes) ]
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
resolveCompletionItem(item : CompletionItem, token : CancellationToken) : CompletionItem | Thenable < CompletionItem > {
|
||||
return item;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GDScriptCompletionItemProvider;
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
DefinitionProvider,
|
||||
TextDocument,
|
||||
Position,
|
||||
CancellationToken,
|
||||
Definition,
|
||||
Location,
|
||||
workspace,
|
||||
Uri,
|
||||
Range
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import config from '../config';
|
||||
import {isStr, getSelectedContent, getStrContent} from './utils';
|
||||
|
||||
class GDScriptDefinitionProivder implements DefinitionProvider {
|
||||
private _rootFolder : string = "";
|
||||
|
||||
constructor(rootFolder: string) {
|
||||
this._rootFolder = rootFolder;
|
||||
}
|
||||
|
||||
provideDefinition(document: TextDocument, position: Position, token: CancellationToken): Definition | Thenable < Definition > {
|
||||
const getDefinitions = (content: string):Location[]| Location => {
|
||||
if(content.startsWith("res://")) {
|
||||
content = content.replace("res://", "");
|
||||
if(workspace && workspace.rootPath) {
|
||||
content = path.join(this._rootFolder, content);
|
||||
}
|
||||
return new Location(Uri.file(content), new Range(0,0,0,0));
|
||||
}
|
||||
else if(fs.existsSync(content) && fs.statSync(content).isFile()) {
|
||||
return new Location(Uri.file(content), new Range(0,0,0,0));
|
||||
}
|
||||
else {
|
||||
const workspaceSymbols = config.getAllSymbols();
|
||||
let locations: Location[] = [];
|
||||
// check from workspace
|
||||
for (let path of Object.keys(workspaceSymbols)) {
|
||||
const script = workspaceSymbols[path];
|
||||
if(path == "autoload" && script.constpathes && script.constpathes[content])
|
||||
path = script.constpathes[content];
|
||||
let scriptitems: Location[] = [];
|
||||
const checkDifinition = (items)=>{
|
||||
const _items: Location[] = [];
|
||||
for (let name of Object.keys(items)) {
|
||||
if(name == content) {
|
||||
_items.push(new Location(Uri.file(path), items[name]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.variables)];
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.constants)];
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.functions)];
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.signals)];
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.classes)];
|
||||
if(script.enumerations)
|
||||
scriptitems = [...scriptitems, ...checkDifinition(script.enumerations)];
|
||||
locations = [...locations, ...scriptitems];
|
||||
}
|
||||
// check from builtin
|
||||
if(config.getClass(content) != null) {
|
||||
const uri = encodeURI('command:vscode.previewHtml?' + JSON.stringify(Uri.parse(`godotdoc://${content}`)));
|
||||
locations.push(new Location(Uri.parse(uri), new Range(0,0,0,0)));
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let selStr = getSelectedContent(document, position);
|
||||
if(selStr) {
|
||||
// For strings
|
||||
if(isStr(selStr)) {
|
||||
selStr = getStrContent(selStr);
|
||||
let fpath = path.join(path.dirname(document.uri.fsPath), selStr)
|
||||
if(fs.existsSync(fpath) && fs.statSync(fpath).isFile())
|
||||
selStr = fpath
|
||||
}
|
||||
resolve(getDefinitions(selStr));
|
||||
}
|
||||
else
|
||||
reject(new Error("Empty selection"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default GDScriptDefinitionProivder;
|
||||
@@ -1,194 +0,0 @@
|
||||
import requestGodot from "../request";
|
||||
import * as vscode from 'vscode';
|
||||
import {DiagnosticCollection, DiagnosticSeverity} from 'vscode';
|
||||
import config from '../config';
|
||||
|
||||
interface GDParseError {
|
||||
message : string,
|
||||
column : number,
|
||||
row : number
|
||||
}
|
||||
|
||||
interface GDScript {
|
||||
members : {
|
||||
constants: {},
|
||||
functions: {},
|
||||
variables: {},
|
||||
signals: {}
|
||||
},
|
||||
base : string,
|
||||
errors : GDParseError[],
|
||||
valid : boolean,
|
||||
is_tool : boolean,
|
||||
native : string
|
||||
}
|
||||
|
||||
interface ParseRequest {
|
||||
text : string,
|
||||
path : string
|
||||
}
|
||||
|
||||
class GDScriptDiagnosticSeverity {
|
||||
private _subscription : DiagnosticCollection;
|
||||
|
||||
constructor() {
|
||||
this._subscription = vscode.languages.createDiagnosticCollection("gdscript")
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._subscription.dispose()
|
||||
}
|
||||
|
||||
async validateScript(doc : vscode.TextDocument, script : any) {
|
||||
if (doc.languageId == 'gdscript') {
|
||||
if (script) {
|
||||
let diagnostics = [ ...(this.validateExpression(doc)), ...(this.validateUnusedSymbols(doc, script)) ];
|
||||
this._subscription.set(doc.uri, diagnostics);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private validateUnusedSymbols(doc : vscode.TextDocument, script) {
|
||||
let diagnostics = [];
|
||||
let cfg : any = vscode.workspace.getConfiguration("GodotTools").get("lint");
|
||||
if (!cfg.unusedSymbols)
|
||||
return diagnostics
|
||||
|
||||
const text = doc.getText();
|
||||
const check = (name : string, range : vscode.Range) => {
|
||||
var matchs = text.match(new RegExp(`([^\\w]|\\[|\\{)\\s*${name}\\s*([^\\w]|\\[|\\{)`, 'g'));
|
||||
let count = matchs ? matchs.length : 0;
|
||||
var incomment = text.match(new RegExp(`#.*?([^\\w]|\\[|\\{)\\s*${name}\\s*([^\\w]|\\[|\\{)`, 'g'));
|
||||
count -= incomment ? incomment.length : 0;
|
||||
if (count <= 1)
|
||||
diagnostics.push(new vscode.Diagnostic(range, `${name} is never used.`, DiagnosticSeverity.Warning));
|
||||
};
|
||||
// Unused variables
|
||||
for (let key of Object.keys(script.variables))
|
||||
check(key, script.variables[key]);
|
||||
for (let key of Object.keys(script.constants))
|
||||
check(key, script.constants[key]);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private validateExpression(doc : vscode.TextDocument) {
|
||||
let cfg : any = vscode.workspace.getConfiguration("GodotTools").get("lint");
|
||||
|
||||
let diagnostics = [];
|
||||
let expectEndOfLine = false;
|
||||
const text = doc.getText();
|
||||
const lines = text.split(/\r?\n/);
|
||||
lines.map((line : string, i : number) => {
|
||||
let matchstart = /[^\s]+.*/.exec(line);
|
||||
let curLineStartAt = 0;
|
||||
if (matchstart)
|
||||
curLineStartAt = matchstart.index;
|
||||
|
||||
// ignore comments
|
||||
if (line.match(/^\s*#.*/) || line.match(/^#.*/))
|
||||
return
|
||||
// normalize line content
|
||||
line = "\t" + line + "\t";
|
||||
var range = new vscode.Range(i, curLineStartAt, i, line.length);
|
||||
|
||||
if (cfg.semicolon && line.match(/[^#].*?\;/) && !line.match(/[#].*?\;/)) {
|
||||
const semicolonIndex = line.indexOf(';');
|
||||
diagnostics.push(new vscode.Diagnostic(new vscode.Range(i, semicolonIndex, i, semicolonIndex + 1), "Statement contains a semicolon.", DiagnosticSeverity.Warning));
|
||||
}
|
||||
if (line.match(/[^#].*?/) && expectEndOfLine) {
|
||||
if (!line.match(/.*?(\\|\:)/)) {
|
||||
diagnostics.push(new vscode.Diagnostic(range, "': or \\' expected at end of the line.", DiagnosticSeverity.Error));
|
||||
expectEndOfLine = false;
|
||||
}
|
||||
if (line.match(/.*?\:/))
|
||||
expectEndOfLine = false;
|
||||
}
|
||||
const colonKeywords = /\b(if|elif|else|for|while|func|class|match)\b/;
|
||||
let keywords = line.match(colonKeywords)
|
||||
if (keywords) {
|
||||
if(line.match(new RegExp(`".*?\\s${keywords[1]}\\s.*?"`)) || line.match(new RegExp(`'.*?\\s${keywords[1]}\\s.*?\'`)))
|
||||
return
|
||||
if(line.match(new RegExp(`.*?#.*?\\s${keywords[1]}\\s.*?`)))
|
||||
return
|
||||
if(line.match(/.*?\sif\s+(\!|\[|\{|\w|").*?\s+else\s+[^\s]+/))
|
||||
return
|
||||
if (line.match(/.*?\\/))
|
||||
expectEndOfLine = true;
|
||||
else if (line.match(/.*?\:[\s+]+[^#\s]+/))
|
||||
return
|
||||
else if (!line.match(/.*?(\\|\:)/))
|
||||
diagnostics.push(new vscode.Diagnostic(range, "': or \\' expected at end of the line.", DiagnosticSeverity.Error));
|
||||
else if (line.match(/\s(if|elif|while|func|class|match)\s*\:/))
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Indentifier expected before ':'", DiagnosticSeverity.Error));
|
||||
else if (line.match(/[^\w]for[^\w]/) && !line.match(/\s+for\s\w+\s+in\s+|[\w+]|\{.*?\}|\[.*?\]|\(.*?\)/)){
|
||||
if(!(line.match(/".*?for.*?"/) || line.match(/'.*?for.*?'/)))
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Invalid for expression", DiagnosticSeverity.Error));
|
||||
}
|
||||
else if (cfg.conditionBrackets && line.match(/\s(if|elif|while|match)\s*\(.*\)\s*:\s*$/))
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Extra brackets in condition expression.", DiagnosticSeverity.Warning));
|
||||
const blockIndetCheck = function() {
|
||||
const err = new vscode.Diagnostic(range, "Expected indented block after expression", DiagnosticSeverity.Error);
|
||||
if (i < lines.length - 1) {
|
||||
let next = i + 1;
|
||||
let nextline = lines[next];
|
||||
// changes nextline until finds a line containg text or comes to the last line
|
||||
while (((!nextline || !nextline.trim().length) || nextline.match(/^\s*#/)) && next < lines.length - 1) {
|
||||
++next;
|
||||
nextline = lines[next];
|
||||
}
|
||||
let nextLineStartAt = -1;
|
||||
let match = /[^\s]+.*/.exec(nextline);
|
||||
if (match)
|
||||
nextLineStartAt = match.index;
|
||||
|
||||
if (nextLineStartAt <= curLineStartAt)
|
||||
diagnostics.push(err);
|
||||
}
|
||||
else if(line.match(/\:\s*$/))
|
||||
diagnostics.push(err);
|
||||
};
|
||||
if(!expectEndOfLine)
|
||||
blockIndetCheck();
|
||||
}
|
||||
// Do not check : for end of statement as it breaks match statment
|
||||
let endOfStateMentWithComma = false;
|
||||
if(endOfStateMentWithComma && !line.match(colonKeywords) && line.match(/\:\s*$/)) {
|
||||
let showErr = true;
|
||||
if( i >= 1 ) {
|
||||
let previous = i - 1;
|
||||
let previousline = lines[previous];
|
||||
while(previousline.match(/\\\s*$/) && previous>=1) {
|
||||
--previous;
|
||||
const ppreviousline = lines[previous];
|
||||
if(ppreviousline.match(/\\\s*$/))
|
||||
previousline = ppreviousline;
|
||||
}
|
||||
const keywords = previousline.match(colonKeywords);
|
||||
if(keywords && !(previousline.match(new RegExp(`".*?\\s${keywords[1]}\\s.*?"`)) || previousline.match(new RegExp(`'.*?\\s${keywords[1]}\\s.*?'`)) ))
|
||||
showErr = false
|
||||
}
|
||||
if(showErr)
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Expected end of statement after expression", DiagnosticSeverity.Error));
|
||||
}
|
||||
if (line.match(/(if|elif|while|return)\s+\w+\s*=\s*\w+/))
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Assignment in condition or return expressions", DiagnosticSeverity.Warning));
|
||||
else if (line.indexOf("==") > 0 && !line.match(/\:\s*/)) {
|
||||
const endAt = line.indexOf("==");
|
||||
const precontent = line.substring(0, endAt);
|
||||
if (!precontent.match(/\s(if|elif|while|return)\s/) && !precontent.match(/=[^=]/) && !precontent.match(/assert\s*\(/) && !expectEndOfLine) {
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Unhandled comparation expression contains", DiagnosticSeverity.Warning));
|
||||
}
|
||||
}
|
||||
let match = /var\s+(\w+)\s*=\s*(\w+)/.exec(line);
|
||||
if (match && match.length > 2 && match[1].length > 0 && match[1] == match[2]) {
|
||||
diagnostics.push(new vscode.Diagnostic(range, "Self Assignment may cause error.", DiagnosticSeverity.Warning));
|
||||
}
|
||||
});
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GDScriptDiagnosticSeverity;
|
||||
@@ -1,325 +0,0 @@
|
||||
import {TextDocumentContentProvider, DocumentLinkProvider, Uri, CancellationToken } from 'vscode';
|
||||
import config from '../config';
|
||||
|
||||
const linkStyle = `
|
||||
<style>
|
||||
a { color: #6e8ae7; text-decoration: none;}
|
||||
</style>
|
||||
`;
|
||||
|
||||
function genLink(title:string, uri:string, span=true):string {
|
||||
const u = encodeURI('command:vscode.previewHtml?' + JSON.stringify(Uri.parse(`godotdoc://${uri}`)));
|
||||
let link = `<a href="${u}">${title}</a>`;
|
||||
if(span)
|
||||
link = `<span>${link}</span>`;
|
||||
return link;
|
||||
};
|
||||
|
||||
function getProp(rawDoc:any, propname: string, action=(s :string)=>s): string {
|
||||
let prop = rawDoc[propname];
|
||||
return action(prop);
|
||||
}
|
||||
|
||||
class GDScriptDocumentContentProvider implements TextDocumentContentProvider{
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide textual content for a given uri.
|
||||
*
|
||||
* The editor will use the returned string-content to create a readonly
|
||||
* [document](TextDocument). Resources allocated should be released when
|
||||
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
|
||||
*
|
||||
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
|
||||
* @param token A cancellation token.
|
||||
* @return A string or a thenable that resolves to such.
|
||||
*/
|
||||
provideTextDocumentContent(uri: Uri, token: CancellationToken): string | Thenable<string> {
|
||||
const request = uri.authority;
|
||||
let classname = request;
|
||||
let membername = null;
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if(request.indexOf(".") != -1) {
|
||||
classname = request.substring(0, request.indexOf("."));
|
||||
if(!request.endsWith("."))
|
||||
membername = request.substring(request.indexOf(".")+1, request.length);
|
||||
}
|
||||
if(classname.length >= 1) {
|
||||
for(let key of config.getBuiltinClassNameList()) {
|
||||
if(key.toLowerCase() == classname) {
|
||||
classname = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(classname && classname.length > 0) {
|
||||
if(membername && membername.length >0 )
|
||||
resolve(self.genMemberDoc(classname, membername)) ;
|
||||
else
|
||||
resolve(self.genClassDoc(config.getClass(classname)));
|
||||
}
|
||||
reject(new Error("Open Documentation Failed!"));
|
||||
});
|
||||
}
|
||||
|
||||
format_documentation(text: string): string {
|
||||
let doc = text.replace(/\[code\]/g, "<code>").replace(/\[\/code\]/g, "</code>");
|
||||
doc = doc.replace(/\[codeblock\]/g, '<pre><code class="gdscript">').replace(/\[\/codeblock]/g, "</code></pre>");
|
||||
doc = doc.replace(/\[i\]/g, "<i>").replace(/\[\/i\]/g, "</i>");
|
||||
doc = doc.replace(/\[b\]/g, "<b>").replace(/\[\/b\]/g, "</b>");
|
||||
doc = doc.replace(/\[u\]/g, "<u>").replace(/\[\/u\]/g, "</u>");
|
||||
doc = doc.replace(/\n\t\t\t\t/g, "\n\t");
|
||||
return doc;
|
||||
};
|
||||
|
||||
genMethodDoc(mDoc:any):string {
|
||||
let ret_type = getProp(mDoc, "return_type", (type:string):string =>{
|
||||
if(type.length > 0)
|
||||
return `${genLink(type,type)} `;
|
||||
else
|
||||
return "void";
|
||||
});
|
||||
let args = "";
|
||||
for(let arg of mDoc.arguments){
|
||||
if(mDoc.arguments.indexOf(arg)!=0)
|
||||
args += ", ";
|
||||
args += `${genLink(arg.type, arg.type)} ${arg.name}`
|
||||
if(arg.default_value && arg.default_value.length > 0)
|
||||
args += `=${arg.default_value}`;
|
||||
}
|
||||
var docContent = mDoc.description;
|
||||
if (!docContent) {
|
||||
docContent = `There is currently no description for this method. Please help us by <span><a href="http://docs.godotengine.org/en/latest/community/contributing/updating_the_class_reference.html">contributing one</a></span>!`;
|
||||
}
|
||||
let doc = `
|
||||
<li>
|
||||
<h4 id="${mDoc.name}">${ret_type} ${mDoc.name} (${args}) <i>${mDoc.qualifiers}</i></h4>
|
||||
<p>${docContent}</p>
|
||||
</li>
|
||||
`;
|
||||
return doc;
|
||||
}
|
||||
|
||||
genPropHeader(mDoc:any, classname:string): string {
|
||||
let type = getProp(mDoc, "type", (type:string):string => `${genLink(type,type)} `);
|
||||
return `<li>${type} ${genLink(mDoc.name, classname+"."+mDoc.name)}</li>`;
|
||||
}
|
||||
|
||||
genMethodHeader(mDoc:any, classname:string):string {
|
||||
let ret_type = getProp(mDoc, "return_type", (type:string):string =>{
|
||||
if(type.length > 0)
|
||||
return `${genLink(type,type)} `;
|
||||
else
|
||||
return "void";
|
||||
});
|
||||
let args = "";
|
||||
for(let arg of mDoc.arguments){
|
||||
if(mDoc.arguments.indexOf(arg)!=0)
|
||||
args += ", ";
|
||||
args += `${genLink(arg.type, arg.type)} ${arg.name}`
|
||||
if(arg.default_value && arg.default_value.length > 0)
|
||||
args += `=${arg.default_value}`;
|
||||
}
|
||||
let doc = `
|
||||
<li>
|
||||
${ret_type} ${genLink(mDoc.name, classname+"."+mDoc.name)} (${args}) <i>${mDoc.qualifiers}</i>
|
||||
</li>
|
||||
`;
|
||||
return doc;
|
||||
}
|
||||
|
||||
|
||||
genPropDoc(pDoc:any): string {
|
||||
let setter = pDoc.setter;
|
||||
if(setter) setter = `<li>Setter: ${setter}(value)</li>`; else setter = "";
|
||||
let getter = pDoc.getter;
|
||||
if(getter) getter = `<li>Getter: ${getter}()</li>`; else getter = "";
|
||||
let descContent = pDoc.description;
|
||||
if(!descContent) {
|
||||
descContent = `There is currently no description for this property. Please help us by <span><a href="http://docs.godotengine.org/en/latest/community/contributing/updating_the_class_reference.html">contributing one</a></span>!`;
|
||||
}
|
||||
let doc = `
|
||||
<li>
|
||||
<h4>${genLink(pDoc.type,pDoc.type)} ${pDoc.name}</h4>
|
||||
<ul>
|
||||
${setter}
|
||||
${getter}
|
||||
</ul>
|
||||
<p>${descContent}</p>
|
||||
</li>
|
||||
`;
|
||||
return doc;
|
||||
}
|
||||
|
||||
genConstDoc(cDoc:any): string {
|
||||
let doc = `
|
||||
<li>
|
||||
<h4>${cDoc.name} = ${cDoc.value}</h4>
|
||||
<p>${cDoc.description}</p>
|
||||
</li>
|
||||
`;
|
||||
return doc;
|
||||
}
|
||||
|
||||
genMemberDoc(classname, membername): string {
|
||||
let realDoc = null;
|
||||
const classdoc = config.getClass(classname);
|
||||
if(!classdoc)
|
||||
return null;
|
||||
for(let m of classdoc.methods) {
|
||||
if(m.name.toLowerCase() == membername) {
|
||||
realDoc = this.genMethodDoc(m);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!realDoc) {
|
||||
for(let s of classdoc.signals) {
|
||||
if(s.name.toLowerCase() == membername) {
|
||||
realDoc = this.genMethodDoc(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!realDoc) {
|
||||
for(let c of classdoc.constants) {
|
||||
if(c.name.toLowerCase() == membername) {
|
||||
realDoc = this.genConstDoc(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!realDoc) {
|
||||
for(let p of classdoc.properties) {
|
||||
if(p.name.toLowerCase() == membername) {
|
||||
realDoc = this.genPropDoc(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!realDoc) {
|
||||
for(let p of classdoc.theme_properties) {
|
||||
if(p.name.toLowerCase() == membername) {
|
||||
realDoc = this.genPropDoc(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!realDoc)
|
||||
return null;
|
||||
|
||||
let doc = `
|
||||
${linkStyle}
|
||||
<h2>Documentation of ${genLink(classname, classname)}.${membername}</h2>
|
||||
<ul>${realDoc}</ul>
|
||||
`;
|
||||
return doc;
|
||||
}
|
||||
|
||||
genClassDoc(rawDoc): string {
|
||||
if(!rawDoc)
|
||||
return null;
|
||||
const classname = rawDoc.name;
|
||||
let inherits = getProp(rawDoc, "inherits", (inherits:string)=>{
|
||||
if (!inherits) return "";
|
||||
return "<h4>Inherits: " + genLink(inherits, inherits, true) +"</h4>";
|
||||
});
|
||||
|
||||
let category = getProp(rawDoc, "category", (c:string)=>{
|
||||
return "<h4>Category: " + c +"</h4>";
|
||||
});
|
||||
|
||||
let subclasses = "";
|
||||
for(let key of config.getBuiltinClassNameList()) {
|
||||
let c = config.getClass(key);
|
||||
if(c && c.inherits == classname) {
|
||||
subclasses += genLink(key, key, true) + " "
|
||||
}
|
||||
};
|
||||
if(subclasses && subclasses.length > 0)
|
||||
subclasses = "<h3>Inherited by</h3> " + "<ul><li>" + subclasses + "</li></ul>";
|
||||
|
||||
let briefDescript = getProp(rawDoc, "brief_description", (dec:string)=>{
|
||||
return dec;
|
||||
});
|
||||
let descript = getProp(rawDoc, "description", (dec:string)=>{
|
||||
if(dec)
|
||||
return "<h3>Description</h3>" + "<ul><li>" + dec + "</li></ul>";
|
||||
else
|
||||
return "";
|
||||
});
|
||||
|
||||
const setter_getters = {};
|
||||
let propHeaders = ""
|
||||
for(let p of rawDoc.properties) {
|
||||
propHeaders += this.genPropHeader(p, classname);
|
||||
if(p.setter)
|
||||
setter_getters[p.setter] = true;
|
||||
if(p.getter)
|
||||
setter_getters[p.getter] = true;
|
||||
}
|
||||
if(propHeaders.length >0)
|
||||
propHeaders = `<h3>Member List</h3><ul>${propHeaders}</ul/>`;
|
||||
|
||||
let methodHeaders = ""
|
||||
let methods = "";
|
||||
for(let m of rawDoc.methods) {
|
||||
if(setter_getters[m.name]) continue;
|
||||
methodHeaders += this.genMethodHeader(m, classname);
|
||||
methods += this.genMethodDoc(m);
|
||||
}
|
||||
if(methodHeaders.length >0)
|
||||
methodHeaders = `<h3>Public Methods</h3><ul>${methodHeaders}</ul/>`;
|
||||
if(methods.length >0 )
|
||||
methods = `<h3>Public Methods</h3><ul>${methods}</ul/>`;
|
||||
|
||||
let signals = "";
|
||||
for(let s of rawDoc.signals) {
|
||||
signals += this.genMethodDoc(s);
|
||||
}
|
||||
if(signals.length >0 )
|
||||
signals = `<h3>Signals</h3><ul>${signals}</ul/>`;
|
||||
|
||||
let props = "";
|
||||
for(let p of rawDoc.properties) {
|
||||
props += this.genPropDoc(p)
|
||||
}
|
||||
for(let p of rawDoc.theme_properties) {
|
||||
props += this.genPropDoc(p)
|
||||
}
|
||||
if(props.length >0 )
|
||||
props = `<h3>Properties</h3><ul>${props}</ul>`
|
||||
|
||||
let constants = "";
|
||||
for(let c of rawDoc.constants) {
|
||||
constants += this.genConstDoc(c);
|
||||
}
|
||||
if(constants.length >0 )
|
||||
constants = `<h3>Constants</h3><ul>${constants}</ul>`
|
||||
|
||||
let doc = `
|
||||
${linkStyle}
|
||||
<h1>Native Class ${classname}</h1>
|
||||
<h4>${briefDescript}</h4>
|
||||
<p>${category}</p>
|
||||
<p>${inherits}</p>
|
||||
<p>${subclasses}</p>
|
||||
<p>${descript}</p>
|
||||
<p>${propHeaders}</p>
|
||||
<p>${methodHeaders}</p>
|
||||
<p>${signals}</p>
|
||||
<p>${constants}</p>
|
||||
<p>${props}</p>
|
||||
<p>${methods}</p>
|
||||
`;
|
||||
return this.format_documentation(doc);
|
||||
}
|
||||
}
|
||||
|
||||
export default GDScriptDocumentContentProvider;
|
||||
@@ -1,195 +0,0 @@
|
||||
import {
|
||||
HoverProvider,
|
||||
TextDocument,
|
||||
Position,
|
||||
CancellationToken,
|
||||
Hover,
|
||||
MarkdownString,
|
||||
workspace,
|
||||
Uri,
|
||||
CompletionItem,
|
||||
CompletionItemKind
|
||||
} from 'vscode';
|
||||
import {
|
||||
isStr,
|
||||
getSelectedContent,
|
||||
getStrContent
|
||||
} from './utils';
|
||||
import config from '../config';
|
||||
import * as path from 'path';
|
||||
|
||||
|
||||
function genLink(title:string, uri:string):string {
|
||||
const u = encodeURI('command:vscode.previewHtml?' + JSON.stringify(Uri.parse(`godotdoc://${uri}`)));
|
||||
return `[${title}](${u})`;
|
||||
};
|
||||
|
||||
class GDScriptHoverProvider implements HoverProvider {
|
||||
constructor() {}
|
||||
|
||||
provideHover(document: TextDocument, position: Position, token: CancellationToken): Hover | Thenable < Hover > {
|
||||
let hoverText = getSelectedContent(document, position);
|
||||
if (isStr(hoverText))
|
||||
hoverText = getStrContent(hoverText);
|
||||
const workspaceSymbols = config.getAllSymbols();
|
||||
let tips: MarkdownString[] = [];
|
||||
const withMarkdwon = workspace.getConfiguration("GodotTools").get("workspaceDocumentWithMarkdown", false);
|
||||
|
||||
|
||||
const makeMarkdown = (content): MarkdownString => {
|
||||
let md = new MarkdownString(content);
|
||||
md.isTrusted = true;
|
||||
return md;
|
||||
}
|
||||
|
||||
// check from workspace
|
||||
const genWorkspaceTips = ()=> {
|
||||
for (let filepath of Object.keys(workspaceSymbols)) {
|
||||
const script = workspaceSymbols[filepath];
|
||||
let scriptips: MarkdownString[] = [];
|
||||
const getHoverText = (items, type, gdpath): MarkdownString[] => {
|
||||
const _items: MarkdownString[] = [];
|
||||
for (let name of Object.keys(items)) {
|
||||
if (name == hoverText) {
|
||||
let dfile = gdpath;
|
||||
if (workspace) {
|
||||
const root = config.normalizePath(workspace.rootPath) + "/";
|
||||
if (gdpath.startsWith(root))
|
||||
dfile = gdpath.replace(root, "");
|
||||
}
|
||||
let signature = "";
|
||||
if(type == "func"|| type == "signal" && script.signatures[name])
|
||||
signature = script.signatures[name];
|
||||
if(type == "const" && script.constvalues[name])
|
||||
signature = ` = ${script.constvalues[name]}`;
|
||||
let doc ='```gdscript\n' + `${type} ${name}${signature}` + '\n```\n';
|
||||
let rowDoc = script.documents[name];
|
||||
if(!withMarkdwon)
|
||||
rowDoc += "```plaintext\r\n"+rowDoc+"\r\n```";
|
||||
doc += rowDoc;
|
||||
doc = doc?doc+"\r\n\r\n":"";
|
||||
if(gdpath != "autoload")
|
||||
doc += `*Defined in [${dfile}](${Uri.file(gdpath).toString()})*`;
|
||||
_items.push(makeMarkdown(doc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
scriptips = [...scriptips, ...getHoverText(script.variables, 'var', filepath)];
|
||||
scriptips = [...scriptips, ...getHoverText(script.constants, 'const', filepath)];
|
||||
scriptips = [...scriptips, ...getHoverText(script.functions, 'func', filepath)];
|
||||
scriptips = [...scriptips, ...getHoverText(script.signals, 'signal', filepath)];
|
||||
scriptips = [...scriptips, ...getHoverText(script.classes, 'class', filepath)];
|
||||
if(script.enumerations)
|
||||
scriptips = [...scriptips, ...getHoverText(script.enumerations, 'const', filepath)];
|
||||
tips = [...tips, ...scriptips];
|
||||
}
|
||||
};
|
||||
|
||||
// check from scnes
|
||||
const genNodePathTips = ()=> {
|
||||
for (let scnenepath of Object.keys(config.nodeInfoMap)) {
|
||||
const nodes: any[] = config.nodeInfoMap[scnenepath];
|
||||
for (let index = 0; index < nodes.length; index++) {
|
||||
const node:any = nodes[index];
|
||||
const fullpath = node.parent + "/" + node.name;
|
||||
if(fullpath == hoverText || fullpath.endsWith(hoverText)) {
|
||||
let filepath = scnenepath;
|
||||
if(workspace && workspace.rootPath)
|
||||
filepath = path.join(workspace.rootPath, filepath);
|
||||
let instance = "";
|
||||
if(node.instance && node.instance.length > 1) {
|
||||
let instancepath = node.instance;
|
||||
if(workspace && workspace.rootPath)
|
||||
instancepath = path.join(workspace.rootPath, instancepath);
|
||||
instance = ` which is an instance of *[${node.instance}](${Uri.file(instancepath).toString()})*`;
|
||||
}
|
||||
tips = [...tips,
|
||||
makeMarkdown(`${genLink(node.type, node.type)} ${fullpath}`),
|
||||
makeMarkdown(`${node.type} defined in *[${scnenepath}](${Uri.file(filepath).toString()})*${instance}`)
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const format_documentation = (text, cls="") => {
|
||||
let doc = text.replace(/\[code\]/g, "`").replace(/\[\/code\]/g, "`");
|
||||
doc = doc.replace(/\[codeblock\]/g, "\n```gdscript\n").replace(/\[\/codeblock]/g, "\n```");
|
||||
doc = doc.replace(/\[i\]/g, "*").replace(/\[\/i\]/g, "*");
|
||||
doc = doc.replace(/\[b\]/g, "**").replace(/\[\/b\]/g, "**");
|
||||
doc = doc.replace(/\[u\]/g, "__").replace(/\[\/u\]/g, "__");
|
||||
doc = doc.replace(/\n\t\t\t\t/g, "\n");
|
||||
return doc;
|
||||
};
|
||||
|
||||
// check from builtin
|
||||
const genBuiltinTips = ()=> {
|
||||
const item2MarkdStrings = (name: string,item: CompletionItem, rowDoc: any):MarkdownString => {
|
||||
let value = "";
|
||||
let doc = format_documentation(item.documentation);
|
||||
// get class name
|
||||
let classname = name;
|
||||
let matchs = name.match(/[@A-z][A-z0-9]*\./);
|
||||
if(matchs) {
|
||||
classname = matchs[0];
|
||||
if(classname.endsWith("."))
|
||||
classname = classname.substring(0, classname.length -1);
|
||||
}
|
||||
|
||||
const genMethodMarkDown = ():string =>{
|
||||
let content = `${genLink(rowDoc.return_type, rowDoc.return_type)} `;
|
||||
if (rowDoc.name != classname) content += `${genLink(classname, classname)}.`;
|
||||
let args = "";
|
||||
for(let arg of rowDoc.arguments){
|
||||
if(rowDoc.arguments.indexOf(arg)!=0)
|
||||
args += ", ";
|
||||
args += `${genLink(arg.type, arg.type)} ${arg.name}`
|
||||
if(arg.default_value && arg.default_value.length > 0)
|
||||
args += `=${arg.default_value}`;
|
||||
}
|
||||
content += `${genLink(rowDoc.name, classname+'.' + rowDoc.name)}(${args}) ${rowDoc.qualifiers}`;
|
||||
return content;
|
||||
};
|
||||
|
||||
switch (item.kind) {
|
||||
case CompletionItemKind.Class:
|
||||
return makeMarkdown(`Native Class ${genLink(classname, classname)}\n${doc}`);
|
||||
case CompletionItemKind.Method:
|
||||
doc = doc.substring(doc.indexOf("\n")+1, doc.length);
|
||||
return makeMarkdown(`${genMethodMarkDown()}\n${doc}`);
|
||||
case CompletionItemKind.Interface:
|
||||
doc = doc.substring(doc.indexOf("\n")+1, doc.length);
|
||||
return makeMarkdown(`signal ${genMethodMarkDown()}\n${doc}`);
|
||||
case CompletionItemKind.Variable:
|
||||
case CompletionItemKind.Property:
|
||||
return makeMarkdown(`${rowDoc.type} ${genLink(classname, classname)}.${genLink(rowDoc.name, classname + "." + rowDoc.name)}\n${doc}`);
|
||||
case CompletionItemKind.Enum:
|
||||
return makeMarkdown(`const ${genLink(classname, classname)}.${genLink(rowDoc.name, classname + "." + rowDoc.name)} = ${rowDoc.value}\n${doc}`);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return makeMarkdown(`${name} ${doc}`);
|
||||
};
|
||||
for (let name of Object.keys(config.builtinSymbolInfoMap)) {
|
||||
const pattern = `[A-z@_]+[A-z0-9_]*\\.${hoverText}\\b`;
|
||||
if(name == hoverText || name.match(new RegExp(pattern))) {
|
||||
const item: {completionItem: CompletionItem, rowDoc: any} = config.builtinSymbolInfoMap[name];
|
||||
tips = [...tips, item2MarkdStrings(name, item.completionItem, item.rowDoc)];
|
||||
}
|
||||
}
|
||||
};
|
||||
genBuiltinTips();
|
||||
genWorkspaceTips();
|
||||
genNodePathTips();
|
||||
|
||||
if (tips.length > 0)
|
||||
return new Hover(tips);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default GDScriptHoverProvider;
|
||||
@@ -1,109 +0,0 @@
|
||||
import {
|
||||
SignatureHelpProvider,
|
||||
TextDocument,
|
||||
Position,
|
||||
CancellationToken,
|
||||
SignatureInformation,
|
||||
SignatureHelp,
|
||||
CompletionItemKind,
|
||||
ParameterInformation,
|
||||
workspace
|
||||
} from 'vscode';
|
||||
import config from '../config';
|
||||
import { countSubStr } from './utils';
|
||||
class GDScriptSignatureHelpProvider implements SignatureHelpProvider {
|
||||
constructor() {}
|
||||
|
||||
provideSignatureHelp(document : TextDocument, position : Position, token : CancellationToken) : SignatureHelp | Thenable < SignatureHelp > {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
const res = self.do_provideSignatureHelp(document, position);
|
||||
resolve(res);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Provide help for the signature at the given position and document.
|
||||
*
|
||||
* @param document The document in which the command was invoked.
|
||||
* @param position The position at which the command was invoked.
|
||||
* @param token A cancellation token.
|
||||
* @return Signature help or a thenable that resolves to such. The lack of a result can be
|
||||
* signaled by returning `undefined` or `null`.
|
||||
*/
|
||||
do_provideSignatureHelp(document : TextDocument, position : Position) : SignatureHelp | Thenable < SignatureHelp > {
|
||||
const range = document.getWordRangeAtPosition(position);
|
||||
let funcname = "";
|
||||
let curparam = 0;
|
||||
const checkPosition = () => {
|
||||
const line = document.lineAt(position);
|
||||
const startPos = line.firstNonWhitespaceCharacterIndex;
|
||||
const endPos = position.character;
|
||||
const queryStr = line.text.substring(startPos, endPos);
|
||||
|
||||
var reg = /([A-z_]+[A-z0-9_]*)\(/g;
|
||||
let match = reg.exec(queryStr);
|
||||
while (match != null) {
|
||||
funcname = match[1];
|
||||
match = reg.exec(queryStr);
|
||||
}
|
||||
if(funcname != "") {
|
||||
const funcrangestr = line.text.substring(line.text.indexOf(queryStr)+queryStr.indexOf(funcname)+funcname.length, endPos);
|
||||
curparam = countSubStr(funcrangestr, ",");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
checkPosition();
|
||||
|
||||
let resultSignatures: SignatureInformation[] = [];
|
||||
|
||||
if (funcname.length > 0) {
|
||||
// Builtin functions
|
||||
for (let key of Object.keys(config.builtinSymbolInfoMap)) {
|
||||
if (key.endsWith(`\.${funcname}`)) {
|
||||
if (config.builtinSymbolInfoMap[key].completionItem.kind == CompletionItemKind.Method || config.builtinSymbolInfoMap[key].completionItem.kind == CompletionItemKind.Function) {
|
||||
const rawDoc = config.builtinSymbolInfoMap[key].rowDoc;
|
||||
const item = config.builtinSymbolInfoMap[key].completionItem;
|
||||
let signatureInfor: SignatureInformation = new SignatureInformation(item.documentation.split('\n')[0], rawDoc.description);
|
||||
for(let arg of rawDoc.arguments){
|
||||
let param: ParameterInformation = new ParameterInformation(`${arg.type} ${arg.name}${arg.default_value.length>0?'='+arg.default_value:''}`, "");
|
||||
signatureInfor.parameters.push(param);
|
||||
}
|
||||
resultSignatures.push(signatureInfor);
|
||||
}
|
||||
}
|
||||
}
|
||||
// workspace functions
|
||||
for (let path of Object.keys(config.getAllSymbols())) {
|
||||
let script = config.getSymbols(path);
|
||||
if(!script.signatures)
|
||||
continue
|
||||
let relaPath = path;
|
||||
if(workspace && workspace.rootPath)
|
||||
relaPath = workspace.asRelativePath(relaPath);
|
||||
|
||||
for(let f of Object.keys(script.signatures)) {
|
||||
if(f == funcname) {
|
||||
const signatureStr = script.signatures[f];
|
||||
let signature: SignatureInformation = new SignatureInformation(`func ${f}${signatureStr}`, `Method defined in ${relaPath}`);
|
||||
const params = (signatureStr.substring(signatureStr.indexOf("(")+1, signatureStr.indexOf(")"))).split(",");
|
||||
for(let p of params)
|
||||
signature.parameters.push(new ParameterInformation(p, ""));
|
||||
resultSignatures.push(signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(resultSignatures.length > 0) {
|
||||
return ({
|
||||
signatures: resultSignatures,
|
||||
activeSignature: 0,
|
||||
activeParameter: curparam
|
||||
});
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GDScriptSignatureHelpProvider;
|
||||
@@ -1,236 +0,0 @@
|
||||
import {Range} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
|
||||
interface GDScript {
|
||||
constants: {},
|
||||
functions: {},
|
||||
variables: {},
|
||||
signals: {},
|
||||
classes: {},
|
||||
base: string,
|
||||
native: string,
|
||||
signatures: {},
|
||||
// symbol: marked string
|
||||
documents: {},
|
||||
// name : value
|
||||
constvalues: {},
|
||||
enumerations: {}
|
||||
}
|
||||
|
||||
class GDScriptSymbolParser {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
parseContent(content: string, ignoreIndentedVars:boolean = false): GDScript {
|
||||
const script: GDScript = {
|
||||
constants: {},
|
||||
functions: {},
|
||||
variables: {},
|
||||
signals: {},
|
||||
classes: {},
|
||||
base: "",
|
||||
native: "",
|
||||
signatures: {},
|
||||
documents: {},
|
||||
constvalues: {},
|
||||
enumerations: {}
|
||||
}
|
||||
const text = content;
|
||||
const lines = text.split(/\r?\n/);
|
||||
|
||||
// Base class and native class
|
||||
for (let line of lines) {
|
||||
let match;
|
||||
if (match = line.match(/extends\s+(\w+)/)) {
|
||||
script.native = match[1];
|
||||
break;
|
||||
} else if (match = line.match(/extends\s+('|")(.*)('|")/)) {
|
||||
script.base = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
const getMatches = (regex:RegExp, index=1) => {
|
||||
var matches = [];
|
||||
for(let line of lines) {
|
||||
let match;
|
||||
if (match = regex.exec(line)) {
|
||||
let commentReg = RegExp(/#.*?/.source+regex.source);
|
||||
if(!commentReg.exec(line))
|
||||
matches.push(match[index]);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
// var matches = [];
|
||||
// var match;
|
||||
// while (match = regex.exec(string)) {
|
||||
// matches.push(match[index]);
|
||||
// }
|
||||
// return matches;
|
||||
};
|
||||
|
||||
const findLineRanges = (symbols, reg)=>{
|
||||
const sm = {};
|
||||
symbols.map((name:string)=>{
|
||||
let line = 0;
|
||||
let curline = 0;
|
||||
if(Object.keys(sm).indexOf(name) != -1) return;
|
||||
lines.map(l=>{
|
||||
const nreg = reg.replace("$X$", name);
|
||||
if(l.match(nreg) != null) {
|
||||
line = curline;
|
||||
return;
|
||||
}
|
||||
curline += 1;
|
||||
});
|
||||
sm[name] = line;
|
||||
});
|
||||
return sm;
|
||||
}
|
||||
|
||||
const determRange = (key:string, array: any): Range =>{
|
||||
let line = array[key];
|
||||
let startAt = lines[line].indexOf(key);
|
||||
if(line < 0) line = 0;
|
||||
if(startAt < 0) startAt = 0;
|
||||
return new Range(line, startAt, line, startAt + key.length);
|
||||
};
|
||||
|
||||
const parseSignature = (range: Range):string => {
|
||||
let res = "";
|
||||
const line = lines[range.start.line];
|
||||
if(line.indexOf("(")!= -1 && line.indexOf(")")!=-1) {
|
||||
const signature = line.substring(line.indexOf("("), line.indexOf(")")+1);
|
||||
if(signature && signature.length >0)
|
||||
res = signature;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const parseDocument = (range: Range):string => {
|
||||
let mdoc = ""
|
||||
let line = range.start.line;
|
||||
while( line > 0){
|
||||
const linecontent = lines[line];
|
||||
let match = linecontent.match(/\s*#\s*(.*)/);
|
||||
let commentAtEnd = linecontent.match(/[\w'",\[\{\]\}\(\)]+\s*#\s*(.*)/) != null;
|
||||
if(commentAtEnd && linecontent.match(/^#/))
|
||||
commentAtEnd = false;
|
||||
if(!match && line != range.start.line)
|
||||
break;
|
||||
if(commentAtEnd && line != range.start.line)
|
||||
break;
|
||||
if(match) {
|
||||
let lmcontent = linecontent.substring(linecontent.indexOf("#")+1, linecontent.length);
|
||||
if(lmcontent.startsWith(" ") && lmcontent != " ")
|
||||
lmcontent = lmcontent.substring(1, lmcontent.length);
|
||||
mdoc = lmcontent + "\r\n" + mdoc;
|
||||
}
|
||||
else if(line != range.start.line)
|
||||
break
|
||||
--line;
|
||||
}
|
||||
return mdoc;
|
||||
}
|
||||
|
||||
let funcsnames = getMatches(/func\s+([_A-Za-z]+[_A-Za-z0-9]*)\s*\(/, 1);
|
||||
const funcs = findLineRanges(funcsnames, "func\\s+$X$\\s*\\(");
|
||||
for (let key of Object.keys(funcs)) {
|
||||
let r: Range = determRange(key, funcs);
|
||||
script.functions[key] = r;
|
||||
script.signatures[key] = parseSignature(r);
|
||||
script.documents[key] = parseDocument(r);
|
||||
}
|
||||
|
||||
let signalnames = getMatches(/signal\s+([_A-Za-z]+[_A-Za-z0-9]*)\s*\(/, 1);
|
||||
const signals = findLineRanges(signalnames, "signal\\s+$X$\\s*\\(");
|
||||
for (let key of Object.keys(signals)) {
|
||||
let r: Range = determRange(key, signals);
|
||||
script.signals[key] = r;
|
||||
script.signatures[key] = parseSignature(r);
|
||||
script.documents[key] = parseDocument(r);
|
||||
}
|
||||
|
||||
let varreg = /var\s+([_A-Za-z]+[_A-Za-z0-9]*)\s*/;
|
||||
let varreg2 = "var\\s+$X$([^\\w]|$)";
|
||||
let vargroup = 1;
|
||||
if(ignoreIndentedVars) {
|
||||
varreg = /^((export.*?var)|var)\s+([_A-Za-z]+[_A-Za-z0-9]*)\s?/;
|
||||
varreg2 = "^((export.*?var)|var)\\s+$X$\\s?";
|
||||
vargroup = 3;
|
||||
}
|
||||
let varnames = getMatches(varreg, vargroup);
|
||||
const vars = findLineRanges(varnames, varreg2);
|
||||
for (let key of Object.keys(vars)){
|
||||
const r:Range = determRange(key, vars)
|
||||
script.variables[key] = r;
|
||||
let newdoc = parseDocument(r);
|
||||
if(newdoc == "" && script.documents[key])
|
||||
newdoc = script.documents[key];
|
||||
script.documents[key] = newdoc;
|
||||
}
|
||||
|
||||
let constnames = getMatches(/const\s+([_A-Za-z]+[_A-Za-z0-9]*)\s*/, 1);
|
||||
const consts = findLineRanges(constnames, "const\\s+$X$\\s*");
|
||||
for (let key of Object.keys(consts)){
|
||||
const r:Range = determRange(key, consts)
|
||||
script.constants[key] = r;
|
||||
let newdoc = parseDocument(r);
|
||||
if(newdoc == "" && script.documents[key])
|
||||
newdoc = script.documents[key];
|
||||
script.documents[key] = newdoc;
|
||||
|
||||
const linecontent = lines[r.start.line];
|
||||
const match = linecontent.match(/const\s+([_A-Za-z]+[_A-Za-z0-9]*)\s*=\s*([\w+]+\(.*\)|"[^"]*"|\-?\d+\.?\d*|\[.*\]|\{.*\})/);
|
||||
if(match && match.length && match.length >1)
|
||||
script.constvalues[key] = match[2];
|
||||
}
|
||||
|
||||
let classnames = getMatches(/class\s+([_A-Za-z]+[_A-Za-z0-9]*)(\s|\:)/, 1);
|
||||
const classes = findLineRanges(classnames, "class\\s+$X$(\\s|\\:)");
|
||||
for (let key of Object.keys(classes)) {
|
||||
const r:Range = determRange(key, classes)
|
||||
script.classes[key] = r;
|
||||
script.documents[key] = parseDocument(r);
|
||||
}
|
||||
|
||||
let enumnames = getMatches(/enum\s+([_A-Za-z]+[_A-Za-z0-9]*)\s+\{/, 1);
|
||||
const enums = findLineRanges(enumnames, "enum\\s+$X$\\s+\{");
|
||||
for (let key of Object.keys(enums)) {
|
||||
const r:Range = determRange(key, enums)
|
||||
script.constants[key] = r;
|
||||
script.documents[key] = parseDocument(r);
|
||||
|
||||
let curindex = r.start.line
|
||||
while (curindex < lines.length) {
|
||||
const line = lines[curindex];
|
||||
let matchs = line.match(/([_A-Za-z]+[_A-Za-z0-9]*)/g);
|
||||
if(matchs && matchs.length >= 1 ){
|
||||
for (var i = 0; i < matchs.length; i++)
|
||||
if(line.indexOf(matchs[i]) > line.indexOf("{"))
|
||||
script.enumerations[matchs[i]] = new Range(curindex, 0, curindex, line.length);
|
||||
}
|
||||
if(line.indexOf("}") == -1)
|
||||
curindex += 1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: enumerations without name
|
||||
// const unnamedEnums = text.match(/enum\s+\{.*\}/gm)
|
||||
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
parseFile(path:string, ignoreIndentedVars:boolean = false): GDScript {
|
||||
const self = this;
|
||||
if(fs.existsSync(path) && fs.statSync(path).isFile()){
|
||||
const content = fs.readFileSync(path, 'utf-8');
|
||||
return this.parseContent(content, ignoreIndentedVars);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GDScriptSymbolParser;
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
DocumentSymbolProvider,
|
||||
TextDocument,
|
||||
SymbolInformation,
|
||||
CancellationToken,
|
||||
SymbolKind,
|
||||
Range,
|
||||
workspace
|
||||
} from 'vscode';
|
||||
|
||||
import GDScriptSymbolParser from '../gdscript/symbolparser';
|
||||
import config from '../config';
|
||||
|
||||
class GDScriptSymbolProvider implements DocumentSymbolProvider {
|
||||
private parser: GDScriptSymbolParser = null;
|
||||
|
||||
constructor() {
|
||||
this.parser = new GDScriptSymbolParser();
|
||||
}
|
||||
|
||||
provideDocumentSymbols(document: TextDocument, token: CancellationToken): SymbolInformation[] | Thenable<SymbolInformation[]> {
|
||||
|
||||
const symbols: SymbolInformation[] = [];
|
||||
var ignoreIndentedVars = false;
|
||||
if(workspace)
|
||||
ignoreIndentedVars = workspace.getConfiguration("GodotTools").get("ignoreIndentedVars", false);
|
||||
const script = this.parser.parseContent(document.getText(), ignoreIndentedVars);
|
||||
const signatures = script.signatures;
|
||||
config.setSymbols(document.fileName, script);
|
||||
|
||||
const funcs = script.functions;
|
||||
for (let key of Object.keys(funcs))
|
||||
symbols.push(new SymbolInformation(key+signatures[key], SymbolKind.Function, funcs[key]));
|
||||
|
||||
const signals = script.signals;
|
||||
for (let key of Object.keys(signals))
|
||||
symbols.push(new SymbolInformation(key+signatures[key], SymbolKind.Interface, signals[key]));
|
||||
|
||||
const vars = script.variables;
|
||||
for (let key of Object.keys(vars))
|
||||
symbols.push(new SymbolInformation(key, SymbolKind.Variable, vars[key]));
|
||||
|
||||
const consts = script.constants;
|
||||
for (let key of Object.keys(consts))
|
||||
symbols.push(new SymbolInformation(key, SymbolKind.Constant, consts[key]));
|
||||
|
||||
const classes = script.classes;
|
||||
for (let key of Object.keys(classes))
|
||||
symbols.push(new SymbolInformation(key, SymbolKind.Class, classes[key]));
|
||||
|
||||
if(script.enumerations) {
|
||||
const enumerations = script.enumerations;
|
||||
for (let key of Object.keys(enumerations))
|
||||
symbols.push(new SymbolInformation(key, SymbolKind.Enum, enumerations[key]));
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GDScriptSymbolProvider;
|
||||
@@ -1,41 +0,0 @@
|
||||
import {TextDocument, Position} from 'vscode';
|
||||
|
||||
export function isStr(content:string) {
|
||||
return (content.startsWith("'") || content.startsWith('"') || content.startsWith('@"') ) && (content.endsWith("'") || content.endsWith('"'));
|
||||
}
|
||||
|
||||
export function getSelectedContent(document: TextDocument, position: Position):string {
|
||||
const line = document.lineAt(position);
|
||||
const wordRange = document.getWordRangeAtPosition(position) ;
|
||||
const machs = line.text.match(/[A-z_]+[A-z_0-9]*|".*?"|'.*?'|@".*?"/g)
|
||||
let res = line.text.substring(wordRange.start.character, wordRange.end.character);
|
||||
machs.map(m=>{
|
||||
if(m) {
|
||||
const startPos = line.text.indexOf(m);
|
||||
const endPos = startPos + m.length;
|
||||
if(isStr(m) && startPos != -1 && wordRange.start.character >= startPos && wordRange.end.character <= endPos){
|
||||
res = m;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
export function getStrContent(rawstr: string):string {
|
||||
let ss = rawstr;
|
||||
if(isStr(ss)) {
|
||||
ss = ss.replace(/"|'|@"|"""/g,"")
|
||||
}
|
||||
return ss;
|
||||
}
|
||||
|
||||
export function countSubStr(str:string, sub:string): number {
|
||||
let count = 0;
|
||||
let pos = str.indexOf(sub);
|
||||
while (pos !== -1) {
|
||||
count++;
|
||||
pos = str.indexOf(sub, pos + sub.length);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import config from '../config';
|
||||
|
||||
class GDScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
|
||||
public provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): vscode.SymbolInformation[] {
|
||||
const scripts = config.getAllSymbols();
|
||||
const symbols: vscode.SymbolInformation[] = [];
|
||||
for (let path of Object.keys(scripts)) {
|
||||
const queryMembers = (query, members, kind: vscode.SymbolKind, path:string, extra=(text)=>text)=> {
|
||||
for (let name of Object.keys(members)) {
|
||||
const range: vscode.Range = members[name];
|
||||
if(name.toLowerCase().indexOf(query.toLowerCase()) != -1) {
|
||||
const symbol: vscode.SymbolInformation = {
|
||||
name: extra(name),
|
||||
kind,
|
||||
containerName: "",
|
||||
location: {
|
||||
uri: vscode.Uri.file(path),
|
||||
range
|
||||
}
|
||||
};
|
||||
symbols.push(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
const scrip = scripts[path];
|
||||
const signatures = scrip.signatures;
|
||||
queryMembers(query, scrip.functions, vscode.SymbolKind.Function, path, (name)=>(name+signatures[name]));
|
||||
queryMembers(query, scrip.signals, vscode.SymbolKind.Interface, path, (name)=>(name+signatures[name]));
|
||||
queryMembers(query, scrip.variables, vscode.SymbolKind.Variable, path);
|
||||
queryMembers(query, scrip.constants, vscode.SymbolKind.Constant, path);
|
||||
queryMembers(query, scrip.classes, vscode.SymbolKind.Class, path);
|
||||
if(scrip.enumerations)
|
||||
queryMembers(query, scrip.enumerations, vscode.SymbolKind.Enum, path);
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
}
|
||||
|
||||
export default GDScriptWorkspaceSymbolProvider;
|
||||
165
src/godot-tools.ts
Normal file
165
src/godot-tools.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import GDScriptLanguageClient, { ClientStatus } from "./lsp/GDScriptLanguageClient";
|
||||
import { get_configuration, set_configuration } from "./utils";
|
||||
|
||||
const CONFIG_CONTAINER = "godot_tools";
|
||||
const TOOL_NAME = "GodotTools";
|
||||
|
||||
export class GodotTools {
|
||||
|
||||
private context: vscode.ExtensionContext;
|
||||
private client: GDScriptLanguageClient = null;
|
||||
private workspace_dir = vscode.workspace.rootPath;
|
||||
private project_file = "project.godot";
|
||||
private connection_status: vscode.StatusBarItem = null;
|
||||
|
||||
constructor(p_context: vscode.ExtensionContext) {
|
||||
this.context = p_context;
|
||||
this.client = new GDScriptLanguageClient(p_context);
|
||||
this.client.watch_status(this.on_client_status_changed.bind(this));
|
||||
this.connection_status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
|
||||
}
|
||||
|
||||
public activate() {
|
||||
vscode.commands.registerCommand("godot-tool.open_editor", ()=>{
|
||||
this.open_workspace_with_editor("-e").catch(err=>vscode.window.showErrorMessage(err));
|
||||
});
|
||||
vscode.commands.registerCommand("godot-tool.run_project", ()=>{
|
||||
this.open_workspace_with_editor().catch(err=>vscode.window.showErrorMessage(err));
|
||||
});
|
||||
vscode.commands.registerCommand("godot-tool.check_status", this.check_client_status.bind(this));
|
||||
|
||||
this.connection_status.text = "$(sync) Initializing";
|
||||
this.connection_status.command = "godot-tool.check_status";
|
||||
this.connection_status.show();
|
||||
this.client.connect_to_server();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public deactivate() {
|
||||
this.client.stop();
|
||||
}
|
||||
|
||||
|
||||
private open_workspace_with_editor(params = "") {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let valid = false
|
||||
if (this.workspace_dir) {
|
||||
let cfg = path.join(this.workspace_dir, this.project_file);
|
||||
valid = (fs.existsSync(cfg) && fs.statSync(cfg).isFile());
|
||||
}
|
||||
if (valid) {
|
||||
this.run_editor(`--path "${this.workspace_dir}" ${params}`).then(()=>resolve()).catch(err=>{
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
reject("Current workspace is not a Godot project");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private run_editor(params = "") {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const run_godot = (path: string, params: string) => {
|
||||
const escape_command = (cmd: string) => {
|
||||
let cmdEsc = `"${cmd}"`;
|
||||
const shell_plugin = vscode.workspace.getConfiguration("terminal.integrated.shell");
|
||||
let shell = shell_plugin ? shell_plugin.get("windows", "") || "" : "";
|
||||
if (shell.endsWith("powershell.exe") && process.platform === "win32") {
|
||||
cmdEsc = `&${cmdEsc}`;
|
||||
}
|
||||
return cmdEsc;
|
||||
};
|
||||
let existingTerminal = vscode.window.terminals.find(t => t.name === TOOL_NAME)
|
||||
if (existingTerminal) {
|
||||
existingTerminal.dispose()
|
||||
}
|
||||
let terminal = vscode.window.createTerminal(TOOL_NAME);
|
||||
let editorPath = escape_command(path);
|
||||
let cmmand = `${editorPath} ${params}`;
|
||||
terminal.sendText(cmmand, true);
|
||||
terminal.show();
|
||||
resolve();
|
||||
};
|
||||
|
||||
let editorPath = get_configuration("editor_path", "")
|
||||
editorPath = editorPath.replace("${workspaceRoot}", this.workspace_dir);
|
||||
if (!fs.existsSync(editorPath) || !fs.statSync(editorPath).isFile()) {
|
||||
vscode.window.showOpenDialog({
|
||||
openLabel: "Run",
|
||||
filters: process.platform === "win32" ? {"Godot Editor Binary": ["exe", "EXE"]} : undefined
|
||||
}).then((uris: vscode.Uri[])=> {
|
||||
if (!uris) return;
|
||||
let path = uris[0].fsPath;
|
||||
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) {
|
||||
reject("Invalid editor path to run the project");
|
||||
} else {
|
||||
run_godot(path, params);
|
||||
set_configuration("editor_path", path);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
run_godot(editorPath, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private check_client_status() {
|
||||
switch (this.client.status) {
|
||||
case ClientStatus.PENDING:
|
||||
vscode.window.showInformationMessage("Connecting to GDScript language server");
|
||||
break;
|
||||
case ClientStatus.CONNECTED:
|
||||
vscode.window.showInformationMessage("Connected to GDScript language server");
|
||||
break;
|
||||
case ClientStatus.DISCONNECTED:
|
||||
this.retry_connect_client();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private on_client_status_changed(status: ClientStatus) {
|
||||
this.connection_status.color = vscode.ThemeColor;
|
||||
switch (status) {
|
||||
case ClientStatus.PENDING:
|
||||
this.connection_status.text = `$(sync) Connecting`;
|
||||
this.connection_status.tooltip = `Connecting to GDScript Language Server`;
|
||||
break;
|
||||
case ClientStatus.CONNECTED:
|
||||
this.connection_status.text = `$(check) Connected`;
|
||||
this.connection_status.tooltip = `Connected to GDScript Language Server`;
|
||||
if (!this.client.started) {
|
||||
this.context.subscriptions.push(this.client.start());
|
||||
}
|
||||
break;
|
||||
case ClientStatus.DISCONNECTED:
|
||||
this.connection_status.text = `$(x) Disconnected`;
|
||||
this.connection_status.tooltip = `Disconnect to GDScript Language Server`;
|
||||
// retry
|
||||
this.retry_connect_client();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private retry_connect_client() {
|
||||
vscode.window.showErrorMessage(`Failed connect to GDScript Language Server`, 'Open Godot Editor', 'Retry', 'Ignore').then(item=>{
|
||||
if (item == 'Retry') {
|
||||
this.client.connect_to_server();
|
||||
} else if (item == 'Open Godot Editor') {
|
||||
this.client.status = ClientStatus.PENDING;
|
||||
this.open_workspace_with_editor("-e").then(()=>{
|
||||
setTimeout(()=>{
|
||||
this.client.connect_to_server();
|
||||
}, 10 * 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
47
src/loggger.ts
Normal file
47
src/loggger.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export class Logger {
|
||||
protected buffer: string = "";
|
||||
protected tag: string = '';
|
||||
protected time: boolean = false;
|
||||
|
||||
constructor(tag: string, time: boolean) {
|
||||
this.tag = tag;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.buffer = "";
|
||||
}
|
||||
|
||||
log(...messages) {
|
||||
|
||||
let line = '';
|
||||
if (this.tag) {
|
||||
line += `[${this.tag}]`;
|
||||
}
|
||||
if (this.time) {
|
||||
line += `[${new Date().toISOString()}]`;
|
||||
}
|
||||
if (line) {
|
||||
line += ' ';
|
||||
}
|
||||
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
line += messages[index];
|
||||
if (index < messages.length) {
|
||||
line += " ";
|
||||
} else {
|
||||
line += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
this.buffer += line;
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
get_buffer(): string {
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger('godot-tools', true);
|
||||
export default logger;
|
||||
141
src/lsp/GDScriptLanguageClient.ts
Normal file
141
src/lsp/GDScriptLanguageClient.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { LanguageClient, LanguageClientOptions, ServerOptions, RequestMessage } from "vscode-languageclient";
|
||||
import { is_debug_mode, get_configuration } from "../utils";
|
||||
import { MessageIO, MessageIOReader, MessageIOWriter, Message } from "./MessageIO";
|
||||
import logger from "../loggger";
|
||||
import { EventEmitter } from "events";
|
||||
import NativeDocumentManager from './NativeDocumentManager';
|
||||
|
||||
function getClientOptions(): LanguageClientOptions {
|
||||
return {
|
||||
// Register the server for plain text documents
|
||||
documentSelector: [
|
||||
{ scheme: "file", language: "gdscript" },
|
||||
{ scheme: "untitled", language: "gdscript" },
|
||||
],
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.gd files contain in the workspace
|
||||
// fileEvents: workspace.createFileSystemWatcher("**/*.gd"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function get_server_uri() : string {
|
||||
let port = get_configuration("gdscript_lsp_server_port", 6008);
|
||||
return `ws://localhost:${port}`;
|
||||
}
|
||||
|
||||
const io = new MessageIO(get_server_uri());
|
||||
const serverOptions: ServerOptions = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({reader: new MessageIOReader(io), writer: new MessageIOWriter(io)});
|
||||
});
|
||||
};
|
||||
|
||||
export enum ClientStatus {
|
||||
PENDING,
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
}
|
||||
const CUSTOM_MESSAGE = "gdscrip_client/";
|
||||
|
||||
export default class GDScriptLanguageClient extends LanguageClient {
|
||||
|
||||
public io: MessageIO = io;
|
||||
|
||||
private context: vscode.ExtensionContext;
|
||||
private _started : boolean = false;
|
||||
private _status : ClientStatus;
|
||||
private _status_changed_callbacks: ((v : ClientStatus)=>void)[] = [];
|
||||
private _initialize_request: Message = null;
|
||||
private message_handler: MessageHandler = null;
|
||||
private native_doc_manager: NativeDocumentManager = null;
|
||||
|
||||
public get started() : boolean { return this._started; }
|
||||
public get status() : ClientStatus { return this._status; }
|
||||
public set status(v : ClientStatus) {
|
||||
if (this._status != v) {
|
||||
this._status = v;
|
||||
for (const callback of this._status_changed_callbacks) {
|
||||
callback(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public watch_status(callback: (v : ClientStatus)=>void) {
|
||||
if (this._status_changed_callbacks.indexOf(callback) == -1) {
|
||||
this._status_changed_callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
super(`GDScriptLanguageClient`, serverOptions, getClientOptions());
|
||||
this.context = context;
|
||||
this.status = ClientStatus.PENDING;
|
||||
this.message_handler = new MessageHandler();
|
||||
this.io.on('disconnected', this.on_disconnected.bind(this));
|
||||
this.io.on('connected', this.on_connected.bind(this));
|
||||
this.io.on('message', this.on_message.bind(this));
|
||||
this.io.on('send_message', this.on_send_message.bind(this));
|
||||
this.native_doc_manager = new NativeDocumentManager(this.io);
|
||||
}
|
||||
|
||||
connect_to_server() {
|
||||
this.status = ClientStatus.PENDING;
|
||||
io.connect_to_language_server();
|
||||
}
|
||||
|
||||
start(): vscode.Disposable {
|
||||
this._started = true;
|
||||
return super.start();
|
||||
}
|
||||
|
||||
private on_send_message(message: Message) {
|
||||
if (is_debug_mode()) logger.log("[client]", JSON.stringify(message));
|
||||
if ((message as RequestMessage).method == "initialize") {
|
||||
this._initialize_request = message;
|
||||
}
|
||||
}
|
||||
|
||||
private on_message(message: Message) {
|
||||
if (is_debug_mode()) logger.log("[server]", JSON.stringify(message));
|
||||
this.message_handler.on_message(message);
|
||||
}
|
||||
|
||||
private on_connected() {
|
||||
if (this._initialize_request) {
|
||||
this.io.writer.write(this._initialize_request);
|
||||
}
|
||||
this.status = ClientStatus.CONNECTED;
|
||||
}
|
||||
|
||||
private on_disconnected() {
|
||||
this.status = ClientStatus.DISCONNECTED;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
class MessageHandler extends EventEmitter {
|
||||
|
||||
changeWorkspace(params: {path: string}) {
|
||||
vscode.window.showErrorMessage("The GDScript Language Server can't work properly!\nThe opening workspace is diffrent with the editor's.", 'Reload', 'Ignore').then(item=>{
|
||||
if (item == "Reload") {
|
||||
let folderUrl = vscode.Uri.file(params.path);
|
||||
vscode.commands.executeCommand('vscode.openFolder', folderUrl, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
on_message(message: any) {
|
||||
if (message && message.method && (message.method as string).startsWith(CUSTOM_MESSAGE)) {
|
||||
const method = (message.method as string).substring(CUSTOM_MESSAGE.length, message.method.length);
|
||||
if (this[method]) {
|
||||
let ret = this[method](message.params);
|
||||
if (ret) {
|
||||
io.writer.write(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/lsp/MessageBuffer.ts
Normal file
87
src/lsp/MessageBuffer.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
|
||||
const DefaultSize: number = 8192;
|
||||
const CR: number = Buffer.from('\r', 'ascii')[0];
|
||||
const LF: number = Buffer.from('\n', 'ascii')[0];
|
||||
const CRLF: string = '\r\n';
|
||||
|
||||
export default class MessageBuffer {
|
||||
|
||||
private encoding: string;
|
||||
private index: number;
|
||||
private buffer: Buffer;
|
||||
|
||||
constructor(encoding: string = 'utf8') {
|
||||
this.encoding = encoding;
|
||||
this.index = 0;
|
||||
this.buffer = Buffer.allocUnsafe(DefaultSize);
|
||||
}
|
||||
|
||||
public append(chunk: Buffer | String): void {
|
||||
var toAppend: Buffer = <Buffer>chunk;
|
||||
if (typeof (chunk) === 'string') {
|
||||
var str = <string>chunk;
|
||||
var bufferLen = Buffer.byteLength(str, this.encoding);
|
||||
toAppend = Buffer.allocUnsafe(bufferLen);
|
||||
toAppend.write(str, 0, bufferLen, this.encoding);
|
||||
}
|
||||
if (this.buffer.length - this.index >= toAppend.length) {
|
||||
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
|
||||
} else {
|
||||
var newSize = (Math.ceil((this.index + toAppend.length) / DefaultSize) + 1) * DefaultSize;
|
||||
if (this.index === 0) {
|
||||
this.buffer = Buffer.allocUnsafe(newSize);
|
||||
toAppend.copy(this.buffer, 0, 0, toAppend.length);
|
||||
} else {
|
||||
this.buffer = Buffer.concat([this.buffer.slice(0, this.index), toAppend], newSize);
|
||||
}
|
||||
}
|
||||
this.index += toAppend.length;
|
||||
}
|
||||
|
||||
public tryReadHeaders(): { [key: string]: string; } | undefined {
|
||||
let result: { [key: string]: string; } | undefined = undefined;
|
||||
let current = 0;
|
||||
while (current + 3 < this.index && (this.buffer[current] !== CR || this.buffer[current + 1] !== LF || this.buffer[current + 2] !== CR || this.buffer[current + 3] !== LF)) {
|
||||
current++;
|
||||
}
|
||||
// No header / body separator found (e.g CRLFCRLF)
|
||||
if (current + 3 >= this.index) {
|
||||
return result;
|
||||
}
|
||||
result = Object.create(null);
|
||||
let headers = this.buffer.toString('ascii', 0, current).split(CRLF);
|
||||
headers.forEach((header) => {
|
||||
let index: number = header.indexOf(':');
|
||||
if (index === -1) {
|
||||
throw new Error('Message header must separate key and value using :');
|
||||
}
|
||||
let key = header.substr(0, index);
|
||||
let value = header.substr(index + 1).trim();
|
||||
result![key] = value;
|
||||
})
|
||||
|
||||
let nextStart = current + 4;
|
||||
this.buffer = this.buffer.slice(nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
}
|
||||
|
||||
public tryReadContent(length: number): string | null {
|
||||
if (this.index < length) {
|
||||
return null;
|
||||
}
|
||||
let result = this.buffer.toString(this.encoding, 0, length);
|
||||
let nextStart = length;
|
||||
this.buffer.copy(this.buffer, 0, nextStart);
|
||||
this.index = this.index - nextStart;
|
||||
return result;
|
||||
}
|
||||
|
||||
public get numberOfBytes(): number {
|
||||
return this.index;
|
||||
}
|
||||
};
|
||||
200
src/lsp/MessageIO.ts
Normal file
200
src/lsp/MessageIO.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { AbstractMessageReader, MessageReader, DataCallback } from "vscode-jsonrpc/lib/messageReader";
|
||||
import { EventEmitter } from "events";
|
||||
import * as WebSocket from 'ws';
|
||||
import MessageBuffer from "./MessageBuffer";
|
||||
import { AbstractMessageWriter, MessageWriter } from "vscode-jsonrpc/lib/messageWriter";
|
||||
import { RequestMessage, ResponseMessage, NotificationMessage } from "vscode-jsonrpc/lib/messages";
|
||||
export type Message = RequestMessage | ResponseMessage | NotificationMessage;
|
||||
|
||||
export class MessageIO extends EventEmitter {
|
||||
|
||||
reader: MessageIOReader = null;
|
||||
writer: MessageIOWriter = null;
|
||||
|
||||
private socket: WebSocket = null;
|
||||
private url: string = "";
|
||||
|
||||
constructor(url: string) {
|
||||
super();
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public send_message(message: string) {
|
||||
if (this.socket) {
|
||||
this.socket.send(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected on_message(chunk: WebSocket.Data) {
|
||||
let message = chunk.toString();
|
||||
this.emit('data', message);
|
||||
}
|
||||
|
||||
on_send_message(message: any) {
|
||||
this.emit("send_message", message);
|
||||
}
|
||||
|
||||
on_message_callback(message: any) {
|
||||
this.emit("message", message);
|
||||
}
|
||||
|
||||
connect_to_language_server():Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = null;
|
||||
const ws = new WebSocket(this.url);
|
||||
ws.on('open', ()=>{ this.on_connected(ws); resolve(); });
|
||||
ws.on('message', this.on_message.bind(this));
|
||||
ws.on('error', this.on_disconnected.bind(this));
|
||||
ws.on('close', this.on_disconnected.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
private on_connected(socket: WebSocket) {
|
||||
this.socket = socket;
|
||||
this.emit("connected");
|
||||
}
|
||||
|
||||
private on_disconnected() {
|
||||
this.socket = null;
|
||||
this.emit('disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class MessageIOReader extends AbstractMessageReader implements MessageReader {
|
||||
|
||||
private io: MessageIO;
|
||||
private callback: DataCallback;
|
||||
private buffer: MessageBuffer;
|
||||
private nextMessageLength: number;
|
||||
private messageToken: number;
|
||||
private partialMessageTimer: NodeJS.Timer | undefined;
|
||||
private _partialMessageTimeout: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.reader = this;
|
||||
this.buffer = new MessageBuffer(encoding);
|
||||
this._partialMessageTimeout = 10000;
|
||||
}
|
||||
|
||||
public set partialMessageTimeout(timeout: number) {
|
||||
this._partialMessageTimeout = timeout;
|
||||
}
|
||||
|
||||
public get partialMessageTimeout(): number {
|
||||
return this._partialMessageTimeout;
|
||||
}
|
||||
|
||||
public listen(callback: DataCallback): void {
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken = 0;
|
||||
this.partialMessageTimer = undefined;
|
||||
this.callback = callback;
|
||||
this.io.on('data', (data: Buffer) => {
|
||||
this.onData(data);
|
||||
});
|
||||
this.io.on('error', (error: any) => this.fireError(error));
|
||||
this.io.on('close', () => this.fireClose());
|
||||
}
|
||||
|
||||
private onData(data: Buffer | String): void {
|
||||
this.buffer.append(data);
|
||||
while (true) {
|
||||
if (this.nextMessageLength === -1) {
|
||||
let headers = this.buffer.tryReadHeaders();
|
||||
if (!headers) {
|
||||
return;
|
||||
}
|
||||
let contentLength = headers['Content-Length'];
|
||||
if (!contentLength) {
|
||||
throw new Error('Header must provide a Content-Length property.');
|
||||
}
|
||||
let length = parseInt(contentLength);
|
||||
if (isNaN(length)) {
|
||||
throw new Error('Content-Length value must be a number.');
|
||||
}
|
||||
this.nextMessageLength = length;
|
||||
// Take the encoding form the header. For compatibility
|
||||
// treat both utf-8 and utf8 as node utf8
|
||||
}
|
||||
var msg = this.buffer.tryReadContent(this.nextMessageLength);
|
||||
if (msg === null) {
|
||||
/** We haven't received the full message yet. */
|
||||
this.setPartialMessageTimer();
|
||||
return;
|
||||
}
|
||||
this.clearPartialMessageTimer();
|
||||
this.nextMessageLength = -1;
|
||||
this.messageToken++;
|
||||
var json = JSON.parse(msg);
|
||||
this.callback(json);
|
||||
// callback
|
||||
this.io.on_message_callback(json);
|
||||
}
|
||||
}
|
||||
|
||||
private clearPartialMessageTimer(): void {
|
||||
if (this.partialMessageTimer) {
|
||||
clearTimeout(this.partialMessageTimer);
|
||||
this.partialMessageTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setPartialMessageTimer(): void {
|
||||
this.clearPartialMessageTimer();
|
||||
if (this._partialMessageTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
this.partialMessageTimer = setTimeout((token, timeout) => {
|
||||
this.partialMessageTimer = undefined;
|
||||
if (token === this.messageToken) {
|
||||
this.firePartialMessage({ messageToken: token, waitingTime: timeout });
|
||||
this.setPartialMessageTimer();
|
||||
}
|
||||
}, this._partialMessageTimeout, this.messageToken, this._partialMessageTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
const ContentLength: string = 'Content-Length: ';
|
||||
const CRLF = '\r\n';
|
||||
export class MessageIOWriter extends AbstractMessageWriter implements MessageWriter {
|
||||
|
||||
private io: MessageIO;
|
||||
private encoding: string;
|
||||
private errorCount: number;
|
||||
|
||||
public constructor(io: MessageIO, encoding: string = 'utf8') {
|
||||
super();
|
||||
this.io = io;
|
||||
this.io.writer = this;
|
||||
this.encoding = encoding;
|
||||
this.errorCount = 0;
|
||||
this.io.on('error', (error: any) => this.fireError(error));
|
||||
this.io.on('close', () => this.fireClose());
|
||||
}
|
||||
|
||||
public write(msg: Message): void {
|
||||
let json = JSON.stringify(msg);
|
||||
let contentLength = Buffer.byteLength(json, this.encoding);
|
||||
|
||||
let headers: string[] = [
|
||||
ContentLength, contentLength.toString(), CRLF,
|
||||
CRLF
|
||||
];
|
||||
try {
|
||||
// callback
|
||||
this.io.on_send_message(msg);
|
||||
// Header must be written in ASCII encoding
|
||||
this.io.send_message(headers.join(''));
|
||||
// Now write the content. This can be written in any encoding
|
||||
this.io.send_message(json);
|
||||
this.errorCount = 0;
|
||||
} catch (error) {
|
||||
this.errorCount++;
|
||||
this.fireError(error, msg, this.errorCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
515
src/lsp/NativeDocumentManager.ts
Normal file
515
src/lsp/NativeDocumentManager.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
import * as vscode from 'vscode';
|
||||
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'
|
||||
}
|
||||
|
||||
interface NativeSymbolInspectParams {
|
||||
native_class: string;
|
||||
symbol_name: string;
|
||||
}
|
||||
|
||||
const enum WebViewMessageType {
|
||||
INSPECT_NATIVE_SYMBOL = 'INSPECT_NATIVE_SYMBOL',
|
||||
};
|
||||
|
||||
class GodotNativeSymbol extends DocumentSymbol {
|
||||
documentation: string;
|
||||
native_class: string;
|
||||
};
|
||||
|
||||
export default class NativeDocumentManager extends EventEmitter {
|
||||
|
||||
private io: MessageIO = null;
|
||||
|
||||
constructor(io: MessageIO) {
|
||||
super();
|
||||
this.io = io;
|
||||
io.on("message", (message: NotificationMessage)=>{
|
||||
if (message.method == Methods.SHOW_NATIVE_SYMBOL) {
|
||||
this.show_native_symbol(message.params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private inspect_native_symbol(params: NativeSymbolInspectParams) {
|
||||
this.io.send_message(JSON.stringify({
|
||||
id: -1,
|
||||
jsonrpc: "2.0",
|
||||
method: Methods.INSPECT_NATIVE_SYMBOL,
|
||||
params
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
private show_native_symbol(symbol: GodotNativeSymbol) {
|
||||
// 创建webview
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'doc',
|
||||
symbol.name,
|
||||
vscode.ViewColumn.Nine,
|
||||
{
|
||||
enableScripts: true, // 启用JS,默认禁用
|
||||
retainContextWhenHidden: false, // webview被隐藏时保持状态,避免被重置
|
||||
}
|
||||
);
|
||||
panel.title = symbol.name;
|
||||
panel.webview.html = this.make_html_content(symbol);
|
||||
panel.webview.onDidReceiveMessage(this.on_webview_message.bind(this));
|
||||
}
|
||||
|
||||
private on_webview_message(msg: any) {
|
||||
switch (msg.type) {
|
||||
case WebViewMessageType.INSPECT_NATIVE_SYMBOL:
|
||||
this.inspect_native_symbol(msg.data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private make_html_content(symbol: GodotNativeSymbol): string {
|
||||
return `
|
||||
<html>
|
||||
<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) {
|
||||
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 {
|
||||
|
||||
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} )`;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: //,
|
||||
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;
|
||||
}
|
||||
`;
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as http from 'http';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
function requestGodot(body : Object) {
|
||||
let postString = JSON.stringify(body);
|
||||
const port = vscode.workspace.getConfiguration("GodotTools").get("editorServerPort", 6996);
|
||||
const options = {
|
||||
hostname: '127.0.0.1',
|
||||
method: 'POST',
|
||||
port,
|
||||
body,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(postString)
|
||||
}
|
||||
};
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
var req = http.request(options, (res) => {
|
||||
let resultString = "";
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => {
|
||||
resultString += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve(JSON.parse(resultString));
|
||||
});
|
||||
});
|
||||
req.on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
req.write(postString);
|
||||
req.end();
|
||||
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
export default requestGodot;
|
||||
@@ -1,283 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import godotRequest from './request';
|
||||
import GDScriptSymbolProvider from './gdscript/symbolprovider';
|
||||
import GDScriptWorkspaceSymbolProvider from './gdscript/workspace_symbol_provider';
|
||||
import GDScriptCompletionItemProvider from './gdscript/completion';
|
||||
import GDScriptDefinitionProivder from './gdscript/definitionprovider';
|
||||
import GDScriptHoverProvider from './gdscript/hoverprovider';
|
||||
import GDScriptDocumentContentProvider from './gdscript/docprovider';
|
||||
import GDScriptSignatureHelpProvider from './gdscript/signature_helper';
|
||||
var glob = require("glob")
|
||||
import config from './config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
class ToolManager {
|
||||
|
||||
private workspaceDir: string = "";
|
||||
private symbolprovider: GDScriptSymbolProvider = null;
|
||||
private workspacesymbolprovider: GDScriptWorkspaceSymbolProvider = null;
|
||||
private _disposable: vscode.Disposable;
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _projectFile : string = "project.godot";
|
||||
private _rootDir : string = "";
|
||||
private _biuitinDocFile : string = "doc/classes-3.0.json";
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
this.workspaceDir = vscode.workspace.rootPath;
|
||||
let completionDollar = false;
|
||||
|
||||
if (vscode.workspace.getConfiguration("GodotTools").get("godotVersion", 3.0) < 3) {
|
||||
this._projectFile = "engine.cfg";
|
||||
this._biuitinDocFile = "doc/classes-2.1.json";
|
||||
completionDollar = true;
|
||||
}
|
||||
this.loadClasses();
|
||||
|
||||
if (vscode.workspace && this.workspaceDir) {
|
||||
vscode.workspace.registerTextDocumentContentProvider('godotdoc', new GDScriptDocumentContentProvider());
|
||||
this.workspaceDir = this.workspaceDir.replace(/\\/g, "/");
|
||||
this._rootDir = vscode.workspace.getConfiguration("GodotTools").get("godotProjectRoot", this.workspaceDir);
|
||||
this._rootDir = this._rootDir.replace("${workspaceRoot}", this.workspaceDir);
|
||||
this.loadWorkspaceSymbols();
|
||||
}
|
||||
|
||||
// documentation symbol provider
|
||||
this.symbolprovider = new GDScriptSymbolProvider();
|
||||
vscode.languages.registerDocumentSymbolProvider('gdscript', this.symbolprovider);
|
||||
// workspace symbol provider
|
||||
this.workspacesymbolprovider = new GDScriptWorkspaceSymbolProvider();
|
||||
vscode.languages.registerWorkspaceSymbolProvider(this.workspacesymbolprovider);
|
||||
// definition provider
|
||||
vscode.languages.registerDefinitionProvider('gdscript', new GDScriptDefinitionProivder(this._rootDir));
|
||||
// hover provider
|
||||
vscode.languages.registerHoverProvider('gdscript', new GDScriptHoverProvider());
|
||||
// code completion provider
|
||||
if (completionDollar)
|
||||
vscode.languages.registerCompletionItemProvider('gdscript', new GDScriptCompletionItemProvider(), '.', '"', "'", "$");
|
||||
else
|
||||
vscode.languages.registerCompletionItemProvider('gdscript', new GDScriptCompletionItemProvider(), '.', '"', "'");
|
||||
// signature help provider
|
||||
vscode.languages.registerSignatureHelpProvider('gdscript', new GDScriptSignatureHelpProvider(), '(', ',');
|
||||
// Commands
|
||||
this._disposable = vscode.Disposable.from(
|
||||
vscode.commands.registerCommand('godot.updateWorkspaceSymbols', this.loadWorkspaceSymbols.bind(this)),
|
||||
vscode.commands.registerCommand('godot.runWorkspace', () => { this.openWorkspaceWithEditor() }),
|
||||
vscode.commands.registerCommand('godot.openWithEditor', () => { this.openWorkspaceWithEditor("-e") }),
|
||||
vscode.commands.registerCommand('godot.runCurrentScene', this.runCurrentScene.bind(this)),
|
||||
);
|
||||
}
|
||||
|
||||
validate() {
|
||||
const self = this;
|
||||
godotRequest({
|
||||
action: "editor",
|
||||
command: "projectdir"
|
||||
}).then((res: any) => {
|
||||
let path = res.path;
|
||||
if (path && path.length > 0 && path.endsWith("/"))
|
||||
path = path.substring(0, path.length - 1)
|
||||
if (path.toLowerCase() == self.workspaceDir.toLowerCase())
|
||||
vscode.window.showInformationMessage("Connected to the Godot editor server");
|
||||
else {
|
||||
vscode.window.showWarningMessage("The opened project is not the same within the Godot editor");
|
||||
}
|
||||
}).catch(e => {
|
||||
vscode.window.showErrorMessage("Failed connecting to the Godot editor server");
|
||||
});
|
||||
}
|
||||
|
||||
loadAllSymbols(): Promise < any > {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
glob(self.workspaceDir + "/**/*.gd", (err, files) => {
|
||||
if (!err) {
|
||||
const symbols = {};
|
||||
for (let i = 0; i < files.length; i++)
|
||||
symbols[config.normalizePath(files[i])] = config.loadSymbolsFromFile(files[i]);
|
||||
// load autoloads from engin.cfg
|
||||
const engincfg = path.join(self.workspaceDir, this._projectFile);
|
||||
if (fs.existsSync(engincfg) && fs.statSync(engincfg).isFile()) {
|
||||
try {
|
||||
const script = {
|
||||
constants: {},
|
||||
functions: {},
|
||||
variables: {},
|
||||
signals: {},
|
||||
classes: {},
|
||||
base: "Object",
|
||||
native: "Object",
|
||||
constpathes: {},
|
||||
documents: {},
|
||||
constvalues: {}
|
||||
};
|
||||
let content: string = fs.readFileSync(engincfg, 'utf-8');
|
||||
if (content && content.indexOf("[autoload]") != -1) {
|
||||
content = content.substring(content.indexOf("[autoload]") + "[autoload]".length, content.length);
|
||||
content = content.substring(0, content.indexOf("["));
|
||||
const lines = content.split(/\r?\n/);
|
||||
lines.map((l) => {
|
||||
if (l.indexOf("=") != 0) {
|
||||
const name = l.substring(0, l.indexOf("="));
|
||||
|
||||
let gdpath = l.substring(l.indexOf("res://") + "res://".length, l.indexOf(".gd") + ".gd".length);
|
||||
gdpath = path.join(this._rootDir, gdpath);
|
||||
let showgdpath = vscode.workspace.asRelativePath(gdpath);
|
||||
|
||||
let doc = "Autoloaded instance of " + `[${showgdpath}](${vscode.Uri.file(gdpath).toString()})`;
|
||||
doc = doc.replace(/"/g, " ");
|
||||
|
||||
script.constants[name] = new vscode.Range(0, 0, 0, 0);
|
||||
script.constvalues[name] = "autoload";
|
||||
script.documents[name] = doc;
|
||||
script.constpathes[name] = gdpath;
|
||||
}
|
||||
});
|
||||
}
|
||||
symbols["autoload"] = script;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
resolve(symbols);
|
||||
} else
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private loadAllNodesInWorkspace() {
|
||||
glob(this.workspaceDir + "/**/*.tscn", (err, files) => {
|
||||
if (!err) {
|
||||
const symbols = {};
|
||||
for (let i = 0; i < files.length; i++)
|
||||
config.loadScene(files[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadWorkspaceSymbols() {
|
||||
let handle = this.showProgress("Loading symbols");
|
||||
if (vscode.workspace.getConfiguration("GodotTools").get("parseTextScene", false)) {
|
||||
this.loadAllNodesInWorkspace();
|
||||
}
|
||||
this.loadAllSymbols().then(symbols => {
|
||||
handle();
|
||||
vscode.window.setStatusBarMessage("$(check) Workspace symbols", 5000);
|
||||
config.setAllSymbols(symbols);
|
||||
}).catch(e => {
|
||||
handle();
|
||||
vscode.window.setStatusBarMessage("$(x) Workspace symbols", 5000);
|
||||
});
|
||||
}
|
||||
|
||||
private openWorkspaceWithEditor(params = "") {
|
||||
let workspaceValid = false
|
||||
if (this.workspaceDir) {
|
||||
let cfg = path.join(this._rootDir, this._projectFile);
|
||||
console.log(cfg);
|
||||
if (fs.existsSync(cfg) && fs.statSync(cfg).isFile())
|
||||
workspaceValid = true;
|
||||
}
|
||||
if (workspaceValid) {
|
||||
let pathFlag = "-path";
|
||||
if (vscode.workspace.getConfiguration("GodotTools").get("godotVersion", 2.1) >= 3)
|
||||
pathFlag = "--path";
|
||||
this.runEditor(`${pathFlag} "${this._rootDir}" ${params}`);
|
||||
}
|
||||
else
|
||||
vscode.window.showErrorMessage("Current workspace is not a Godot project");
|
||||
}
|
||||
|
||||
private runEditor(params = "") {
|
||||
let editorPath = vscode.workspace.getConfiguration("GodotTools").get("editorPath", "")
|
||||
editorPath = editorPath.replace("${workspaceRoot}", this.workspaceDir);
|
||||
console.log(editorPath);
|
||||
if (!fs.existsSync(editorPath) || !fs.statSync(editorPath).isFile()) {
|
||||
vscode.window.showErrorMessage("Invalid editor path to run the project");
|
||||
} else {
|
||||
let existingTerminal = vscode.window.terminals.find(t => t._name === "GodotTools")
|
||||
if (existingTerminal) {
|
||||
existingTerminal.dispose()
|
||||
}
|
||||
let terminal = vscode.window.createTerminal("GodotTools");
|
||||
editorPath = this.escapeCmd(editorPath);
|
||||
let cmmand = `${editorPath} ${params}`;
|
||||
terminal.sendText(cmmand, true);
|
||||
terminal.show();
|
||||
}
|
||||
}
|
||||
|
||||
private runCurrentScene() {
|
||||
const absFilePath = vscode.window.activeTextEditor.document.uri.fsPath;
|
||||
let scenePath = null
|
||||
if (vscode.window.activeTextEditor) {
|
||||
scenePath = path.relative(this._rootDir, absFilePath);
|
||||
scenePath = scenePath.replace(/\\/g, "/");
|
||||
}
|
||||
// Run scripts directly which is inhired from SceneTree or MainLoop
|
||||
if (scenePath.endsWith(".gd")) {
|
||||
const scriptPath = scenePath;
|
||||
scenePath = config.scriptSceneMap[config.normalizePath(scenePath)];
|
||||
if (!scenePath) {
|
||||
const script = config.loadSymbolsFromFile(absFilePath);
|
||||
if (script) {
|
||||
if(script.native == "SceneTree" || script.native == "MainLoop") {
|
||||
this.runEditor(`-s "${absFilePath}"`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scenePath) {
|
||||
if (scenePath.endsWith(".gd"))
|
||||
scenePath = ` -s "res://${scenePath}" `;
|
||||
else
|
||||
scenePath = ` "res://${scenePath}" `;
|
||||
this.openWorkspaceWithEditor(scenePath);
|
||||
} else
|
||||
vscode.window.showErrorMessage("Current document is not a scene file or MainLoop");
|
||||
}
|
||||
|
||||
loadClasses() {
|
||||
let done: boolean = false;
|
||||
if (this.workspaceDir)
|
||||
done = config.loadClasses(path.join(this.workspaceDir, ".vscode", "classes.json"));
|
||||
if (!done)
|
||||
done = config.loadClasses(path.join(this._context.extensionPath, this._biuitinDocFile));
|
||||
if (!done)
|
||||
vscode.window.showErrorMessage("Loading GDScript documentation failed");
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
private showProgress(message: string) {
|
||||
let r_resolve;
|
||||
vscode.window.withProgress({ location: vscode.ProgressLocation.Window}, p => {
|
||||
return new Promise((resolve, reject) => {
|
||||
p.report({message});
|
||||
r_resolve = resolve;
|
||||
});
|
||||
});
|
||||
return r_resolve;
|
||||
}
|
||||
|
||||
private escapeCmd(cmd: string) {
|
||||
// Double quote command (should work in at least cmd.exe and bash)
|
||||
let cmdEsc = `"${cmd}"`;
|
||||
|
||||
// Fetch Windows shell type
|
||||
let shell = vscode.workspace.getConfiguration("terminal.integrated.shell").get("windows", "");
|
||||
|
||||
// For powershell we prepend an & to prevent the command being treated as a string
|
||||
if (shell.endsWith("powershell.exe") && process.platform === "win32") {
|
||||
cmdEsc = `&${cmdEsc}`;
|
||||
}
|
||||
return cmdEsc
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default ToolManager;
|
||||
15
src/utils.ts
Normal file
15
src/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as vscode from "vscode";
|
||||
|
||||
const CONFIG_CONTAINER = "godot_tools";
|
||||
|
||||
export function get_configuration(name: string, default_value: any = null) {
|
||||
return vscode.workspace.getConfiguration(CONFIG_CONTAINER).get(name, default_value) || default_value;
|
||||
}
|
||||
|
||||
export function set_configuration(name: string, value: any) {
|
||||
return vscode.workspace.getConfiguration(CONFIG_CONTAINER).update(name, value);
|
||||
}
|
||||
|
||||
export function is_debug_mode(): boolean {
|
||||
return process.env.VSCODE_DEBUG_MODE === "true";
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import {Disposable, window} from 'vscode';
|
||||
import GDScriptDiagnosticSeverity from './gdscript/diagnostic';
|
||||
import GDScriptCompleter from './gdscript/completion';
|
||||
import config from './config';
|
||||
|
||||
interface DocumentFlag {
|
||||
path: string,
|
||||
version: number
|
||||
}
|
||||
|
||||
class WindowWatcher {
|
||||
|
||||
private _disposable: Disposable;
|
||||
private _diagnosticSeverity: GDScriptDiagnosticSeverity;
|
||||
private _lastText: DocumentFlag;
|
||||
|
||||
constructor() {
|
||||
let subscriptions: Disposable[] = [];
|
||||
window.onDidChangeTextEditorSelection(this.onDidChangeTextEditorSelection.bind(this), this, subscriptions);
|
||||
window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor.bind(this), this, subscriptions);
|
||||
window.onDidChangeTextEditorOptions(this.onDidChangeTextEditorOptions.bind(this), this, subscriptions);
|
||||
window.onDidChangeTextEditorViewColumn(this.onDidChangeTextEditorViewColumn.bind(this), this, subscriptions);
|
||||
|
||||
this._diagnosticSeverity = new GDScriptDiagnosticSeverity();
|
||||
this._disposable = Disposable.from(...subscriptions, this._diagnosticSeverity);
|
||||
this._lastText = {path: "-1", version: -1};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the [active editor](#window.activeTextEditor)
|
||||
* has changed. *Note* that the event also fires when the active editor changes
|
||||
* to `undefined`.
|
||||
*/
|
||||
private onDidChangeActiveTextEditor(event: any) {
|
||||
// console.log("[GodotTools]:onDidChangeActiveTextEditor", event);
|
||||
if(window.activeTextEditor != undefined) {
|
||||
const doc = window.activeTextEditor.document;
|
||||
const script = config.loadSymbolsFromFile(doc.fileName);
|
||||
if (vscode.workspace.getConfiguration("GodotTools").get("enableSyntaxChecking", true)) {
|
||||
this._diagnosticSeverity.validateScript(doc, script).then(()=>{});
|
||||
}
|
||||
this._lastText = {path: doc.fileName, version: doc.version};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the selection in an editor has changed.
|
||||
*/
|
||||
private onDidChangeTextEditorSelection(event: any) {
|
||||
// console.log("[GodotTools]:onDidChangeTextEditorSelection");
|
||||
const doc = window.activeTextEditor.document;
|
||||
const curText: DocumentFlag= {path: doc.fileName, version: doc.version};
|
||||
// Check content changed
|
||||
if(this._lastText.path != curText.path || this._lastText.version != curText.version) {
|
||||
const script = config.loadSymbolsFromFile(doc.fileName);
|
||||
if (vscode.workspace.getConfiguration("GodotTools").get("enableSyntaxChecking", true)) {
|
||||
this._diagnosticSeverity.validateScript(doc, script).then(()=>{});
|
||||
}
|
||||
this._lastText = curText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the options of an editor have changed.
|
||||
*/
|
||||
private onDidChangeTextEditorOptions(event: any) {
|
||||
// console.log("[GodotTools]:onDidChangeTextEditorOptions", event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the view column of an editor has changed.
|
||||
*/
|
||||
private onDidChangeTextEditorViewColumn(event: any) {
|
||||
// console.log("[GodotTools]:onDidChangeTextEditorViewColumn", event);
|
||||
}
|
||||
}
|
||||
|
||||
export default WindowWatcher;
|
||||
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test",
|
||||
"src/gdscript/server",
|
||||
"server"
|
||||
]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": false
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
14
tslint.json
Normal file
14
tslint.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
"no-duplicate-variable": false,
|
||||
"curly": true,
|
||||
"class-name": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
||||
Reference in New Issue
Block a user