Checkout new GDScriptLanguageClient branch

This commit is contained in:
geequlim
2019-05-04 19:48:36 +08:00
parent 2ba776dc40
commit ece1f3118d
36 changed files with 292 additions and 179167 deletions

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true

36
.vscode/launch.json vendored
View File

@@ -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",
"version": "0.2.0",
"configurations": [{
"name": "Run 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"
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch",
"env": {
"VSCODE_DEBUG_MODE": true
}
},
]
}

View File

@@ -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"
}

44
.vscode/tasks.json vendored
View File

@@ -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
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -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

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016-2017 Geequlim
Copyright (c) 2016-2019 Geequlim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

File diff suppressed because one or more lines are too long

View File

@@ -96,7 +96,7 @@
},
"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",
"match": "\\b(?i:elif|else|for|if|while|break|continue|pass|in|is|return|onready|setget|enum|match|breakpoint|tool|extends|signal|class|class_name)\\b",
"name": "keyword.control.gdscript"
},
"letter": {

2
doc/.gitignore vendored
View File

@@ -1,2 +0,0 @@
classes/*.xml
classes-*.xml

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +1,39 @@
{
"name": "godot-tools",
"displayName": "Godot Tools",
"displayName": "godot-tools",
"icon": "icon.png",
"version": "1.0.0",
"description": "Tools for game development with godot game engine",
"version": "0.3.7",
"publisher": "geequlim",
"repository": "https://github.com/GodotExplorer/godot-tools",
"license": "MIT",
"publisher": "geequlim",
"engines": {
"vscode": "^1.1.21"
"vscode": "^1.33.0"
},
"categories": [
"Other"
],
"activationEvents": [
"workspaceContains:engine.cfg",
"workspaceContains:project.godot",
"onLanguage:gdscript"
],
"main": "./out/src/extension",
"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.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"
}
],
"commands": [],
"configuration": {
"type": "object",
"title": "Godot Tools configuration",
"properties": {
"GodotTools.maxNumberOfProblems": {
"godot_tools.gdscript_lsp_server_port": {
"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"
"default": 6008,
"description": "The websocket server port of the GDScript Langugae Protocol server"
}
}
},
@@ -136,22 +75,15 @@
}
]
},
"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"
},
"dependencies": {},
"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"
"@types/mocha": "^2.2.42",
"@types/node": "^10.12.21",
"@types/ws": "^6.0.1",
"tslint": "^5.16.0",
"typescript": "^3.4.5",
"vscode": "^1.1.33",
"vscode-languageclient": "^5.2.1",
"websocket-stream": "^5.5.0"
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -1,13 +1,16 @@
'use strict';
import { workspace, Disposable, ExtensionContext } from 'vscode';
import WindowWatch from "./window_watcher";
import ToolManager from './tool_manager';
import { ExtensionContext } from "vscode";
import GDScriptLanguageClient from "./lsp/GDScriptLanguageClient";
let tool: ToolManager = null;
let client: GDScriptLanguageClient = null;
export function activate(context: ExtensionContext) {
tool = new ToolManager(context);
context.subscriptions.push(tool);
context.subscriptions.push(new WindowWatch());
console.log("[GodotTools]: Extension Activated");
client = new GDScriptLanguageClient();
context.subscriptions.push(client.start());
}
export function deactivate(): Thenable<void> {
if (client) {
return client.stop();
}
return new Promise((resolve, reject) => {});
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

47
src/loggger.ts Normal file
View 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;

View File

@@ -0,0 +1,48 @@
import { workspace } from "vscode";
import * as websocket from "websocket-stream";
import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient";
import { is_debug_mode, get_configuration } from "../utils";
import logger from "../loggger";
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"),
},
};
}
const serverOptions: ServerOptions = () => {
return new Promise((resolve, reject) => {
let port = get_configuration("gdscript_lsp_server_port", 6008);
const ws = websocket(`ws://localhost:${port}`);
if (is_debug_mode()) {
let text = '';
ws.on('data', (chunk)=>{
let message = chunk.toString();
text += message;
logger.log("[server]", message);
});
const origin_write = ws._write.bind(ws);
ws._write = (function (chunk: any, encoding: string, callback: (error?: Error | null) => void) {
let message = chunk.toString();
text += message;
origin_write(chunk, encoding, callback);
logger.log("[client]", message);
}).bind(ws);
}
resolve({reader: ws, writer: ws});
});
};
export default class GDScriptLanguageClient extends LanguageClient {
constructor() {
super(`GDScriptLanguageClient`, serverOptions, getClientOptions());
}
};

View File

@@ -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;

View File

@@ -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;

9
src/utils.ts Normal file
View File

@@ -0,0 +1,9 @@
import { workspace } from "vscode";
export function get_configuration(name: string, default_value: any = null) {
return workspace.getConfiguration("godot_tools").get(name, default_value);
}
export function is_debug_mode(): boolean {
return process.env.VSCODE_DEBUG_MODE === "true";
}

View File

@@ -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;

View File

@@ -7,12 +7,10 @@
"es6"
],
"sourceMap": true,
"rootDir": "."
"rootDir": "src",
"strict": false
},
"exclude": [
"node_modules",
".vscode-test",
"src/gdscript/server",
"server"
]
}

14
tslint.json Normal file
View 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"
}