Compare commits

...

45 Commits

Author SHA1 Message Date
Geequlim
2a467ed990 Release version 1.0.1 2020-01-30 00:12:21 +08:00
Geequlim
60937ad776 Fix run editor error on windows with default terminal configurations 2020-01-30 00:11:23 +08:00
Geequlim
0ac1299f47 Release 1.0.0 2020-01-29 20:44:33 +08:00
Geequlim
cf22aa3595 Merge pull request #138 from sevkin/137
check before calling platform-specific code
2020-01-26 16:27:00 +08:00
Vsevolod Balashov
c41bd642f0 check before calling platform-specific code
closes #137
2020-01-26 02:29:34 +03:00
geequlim
8d8f5c2d9b Support reconnect to language server when server port changed 2020-01-12 00:00:17 +08:00
Geequlim
119a7ebd23 Merge pull request #134 from Bromeon/bugfix/highlight-camel-case
Fix syntax highlighting for camelCase identifiers
2020-01-10 23:54:50 +08:00
Jan Haller
555cb1ce9a Fix syntax highlighting for camelCase identifiers
Currently, any occurrences of PascalCase identifiers (even as parts of other words) are recognized as classes.
This assumes snake_case convention for all methods and variables and makes it impossible to use camelCase. While this is the recommended GDScript style, the syntax highlighter should allow for different styles as long as it can do so unambiguously. This is already done for existing rules, but overridden by one rule with an overly general regex pattern.

This commit modifies the catch-all rule for the 'parscal_class' group to only capture whole words.
For clarity, it renames 'parscal_class' to 'pascal_case_class'.

Other groups to recognize classes remain unchanged: type_declear, function-return-type, class_def, class_new, class_is, class_enum, class_name, extends
2019-12-29 18:00:31 +01:00
geequlim
ca3a1e62c4 Fix status icon color bug 2019-12-07 14:03:59 +08:00
geequlim
61e05e7d6e Update dependencies 2019-12-07 14:03:38 +08:00
Geequlim
96b833851d Merge pull request #124 from Calinou/improve-messages
Improve messages for consistency and fix typos
2019-11-06 00:25:49 +08:00
Hugo Locurcio
602cc895c0 Improve messages for consistency and fix typos 2019-11-04 18:52:53 +01:00
geequlim
41a58d76b1 Fix indent in GDScript.tmLaguage.json 2019-10-27 12:04:42 +08:00
Geequlim
8d6992822e Merge pull request #123 from OrenjiAkira/master
Fix class_name statement bug
2019-10-27 12:00:41 +08:00
orenjiakira
f0914742e1 Fix class_name statement bug 2019-10-26 16:07:09 -03:00
Geequlim
a1e020560d Merge pull request #120 from Calinou/improve-readme
Improve the README and fix various typos
2019-10-10 23:31:51 +08:00
Hugo Locurcio
cf9e478b12 Improve the README and fix various typos 2019-10-10 17:29:43 +02:00
Geequlim
d155016b08 Merge pull request #121 from Calinou/optimize-images
Optimize images losslessly using `oxipng -o6 --strip --zopfli`
2019-10-10 23:28:36 +08:00
Hugo Locurcio
40b09491ac Optimize images losslessly using oxipng -o6 --strip --zopfli 2019-10-10 17:04:21 +02:00
Geequlim
24c29452c7 Fix typo in readme 2019-10-09 15:15:25 +08:00
Geequlim
ecffd631a8 Update version to 1.0.0 ! 2019-10-09 14:47:21 +08:00
Geequlim
3645e431d3 Add command to list godot native classes 2019-10-09 13:30:21 +08:00
Geequlim
e2febb81b1 Setup class inherit tree and render in native documentation page 2019-10-09 13:14:04 +08:00
Geequlim
f07e1154ef Improve native documentation webview renderer
Make prism as a custom lib instead of node module to reduce the binary size
2019-10-08 19:34:47 +08:00
geequlim
a563a3584a Bump to 0.9.1 2019-10-06 17:09:09 +08:00
geequlim
758aafc570 Render docs of native symbols in webview mostly works now 2019-10-06 17:06:49 +08:00
geequlim
eba90dbbf9 Jump between native documentations 2019-10-04 20:54:47 +08:00
geequlim
47647a05ae [WIP] Add NativeDocumentManager to show native symbol informations 2019-10-04 19:22:02 +08:00
geequlim
28e284f0ad Improve code hlighting 2019-10-04 14:15:42 +08:00
Geequlim
c26320ec03 Merge pull request #118 from pduzinki:fix-string-highlighting
fix string literal highlighting
2019-10-04 13:05:26 +08:00
pawel duzinkiewicz
7d20df3b35 fix string literal highlighting 2019-10-03 19:58:38 +02:00
Geequlim
a84548aeac Merge pull request #116 from anoadragon453/patch-2
typo fix
2019-09-27 11:12:04 +08:00
Andrew Morgan
0ae80d6bcd typo fix
small typo fix
2019-09-26 23:54:47 +01:00
geequlim
325b17a29c transfer to godotengine/godot-vscode-plugin 2019-08-28 22:36:45 +08:00
geequlim
0938b6384a Fix crash with detect shell configurations of VSCode settings 2019-08-28 22:15:42 +08:00
Geequlim
3064849452 Fix error with restart godot editor
Move more client related code to GDScriptLanguageClient
2019-06-26 17:29:06 +08:00
Geequlim
c416ea6789 Allow retry connect to editor after ignored retry 2019-06-26 12:08:10 +08:00
Geequlim
1e22ac0d9a Fix release build 2019-06-25 17:33:25 +08:00
Geequlim
61e1cafdfe The GDScript LSP Client is works correctly ! 2019-06-25 17:16:59 +08:00
Geequlim
1844dd570b Merge pull request #102 from ankitpriyarup/lsp-client
minor addition in configuration and grammar files
2019-05-31 19:17:31 +08:00
Ankit Priyarup
ff7f31776a minor addition in configuration and grammar files
comment folding, addition of new keywords in grammar, removing wrong comments from snippets which was causing problem
2019-05-31 15:13:40 +05:30
geequlim
fafabc3b34 improve syntax highlight of GDScript 2019-05-22 22:20:54 +08:00
Geequlim
d6331fee89 Implement MessageTransports for the websocket connection
Drop stream wrap fro the websocket connection
2019-05-21 10:47:34 +08:00
geequlim
41b36e6e95 Warp the websocket connection to a mock stream 2019-05-19 18:46:14 +08:00
geequlim
ece1f3118d Checkout new GDScriptLanguageClient branch 2019-05-04 19:48:36 +08:00
49 changed files with 2197 additions and 179253 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

46
.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",
"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
}
},
]
}

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

48
.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
"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
}
}
]
}

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,5 +1,13 @@
# Change Log
### 1.0.1
* Fix run editor error on windows with default terminal configurations
### 1.0.0
* Refactor the whole plugin with gdscript language server support
* Add webview renderer to show documentations of native symbols.
* Only support godot 3.2 and above
### 0.3.7
* Add `lint` configuration to control the behaviors of syntax checking
* Fix error with run godot editor when the editor contains spaces
@@ -16,7 +24,7 @@
```
### 0.3.6
* Fix project configuartion file path
* Fix project configuration file path
### 0.3.5
* Add option to disable syntax checking for GDScript
@@ -56,7 +64,7 @@
* Limited code completions
### 0.2.9
* Add configuration `GodotTools.completeNodePath` to switch is complete node pathes
* Add configuration `GodotTools.completeNodePath` to switch is complete node paths
* Enhanced syntax highlight with GDScript
* Enhanced code completion with GDScript
@@ -69,7 +77,7 @@
* Fix some error with syntax checking
* Add symbol support for enumerations
* Remove key bindings for `F5`~`F8` as it might be confict with other functionalities of VSCode
* Remove key bindings for `F5`~`F8` as it might be conflict with other functionalities of VSCode
* You can bind the key bindings back by add following configurations
```json
{
@@ -105,14 +113,14 @@
* Run games within VSCode terminals
* Add key bindings for `F5 to run the workspace` and `F6 to run the edting scene`
* Fix a lot of bugs with unused vaiable cheching
* Fix a lot of bugs with unused variable checking
* Move workspace symbols state notice to status bar
### 0.2.4
* Add code cheching for asignments and comparations
* Impoved builtin documentation preview page
* Fix bugs with unused vaiable cheching
* Add code checking for asignments and comparisons
* Improved builtin documentation preview page
* Fix bugs with unused variable checking
### 0.2.3
* Fix known errors with code syntax checking
@@ -122,7 +130,7 @@
### 0.2.2
* Better Syntax validating for code blocks
* More waring for non-python liked expression
* More warning for non-python liked expression
### 0.2.1
* Support markdown render in hover tips for documentations in workspace symbols
@@ -130,14 +138,14 @@
### 0.2.0
* Show autoloads informations in hover tips and go to autoloads' definitions are supported now
* Show autoloads information in hover tips and go to autoloads' definitions are supported now
* Fix the bug that workspace symbols resoved twice on Windows
### 0.1.9
* Show workspace constant value in hover tips and completion items
* More readable style for links in documentation preview page
* Improve code completion sort order and auto insert `()` for functions without paramaters
* Improve code completion sort order and auto insert `()` for functions without parameters
* Fix bugs with workspace documentation parsing
### 0.1.8
@@ -154,7 +162,7 @@
* Reorder mouse hover tips, builtin methods are at top of workspace methods
* Show callabel signatures with documente symbols and workspace symbols
* Syntax highlight support for signal paramaters
* Syntax highlight support for signal parameters
### 0.1.5

View File

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

View File

@@ -1,64 +1,79 @@
A complete set of tools to code games with the [Godot game engine](http://www.godotengine.org/) in Visual Studio Code.
# Godot Tools
A complete set of tools to code games with
[Godot Engine](http://www.godotengine.org/) in Visual Studio Code.
**IMPORTANT NOTE:** Versions 1.0.0 and later of this plugin only support
Godot 3.2 or later.
## Features
The extension comes with a wealth of features to make your Godot programming experience as comfortable as possible:
The extension comes with a wealth of features to make your Godot programming
experience as comfortable as possible:
- Syntax highlighting for the GDscript (`.gd`) language
- Syntax highlighting for the GDScript (`.gd`) language
- Syntax highlighting for the `.tscn` and `.tres` scene formats
- Full typed GDScript support
- Optional "Smart Mode" to improve productivity with dynamically typed scripts
- Function definitions and documentation display on hover (see image below)
- Rich auto-completion
- Static code validation
- Open projects and scenes in Godot from VS Code
- Ctrl-click on a variable or method call to jump to its definition
- Full documentation of the Godot engine's API supported
- Rich autocompletion
- Display script warnings and errors
- Ctrl + click on a variable or method call to jump to its definition
- Full documentation of the Godot Engine's API supported
- Run a Godot project from VS Code
![Showing the documentation on hover feature](img/godot-tools.jpg)
![Showing the documentation on hover feature](img/godot-tools.png)
## Available Commands
## Available commands
The extension adds a few entries to the VS Code Command Palette under "GodotTools":
The extension adds a few entries to the VS Code Command Palette under "Godot Tools":
- Update workspace symbols
- Run workspace as Godot project
- Open workspace with Godot editor
- Run current scene
- Run the workspace as a Godot project
- List Godot's native classes
## Settings
### Godot
If you like this extension, you can set VS Code as your default script editor for Godot by following these steps:
1. Open editor settings
2. Select `Text Editor / External`
3. Make sure the `Use External Editor` box is checked
4. Fill `Exec Path` with the path to your VS Code executable
5. Fill `Exec Flags` with `{project} --goto {file}:{line}:{col}`
If you like this extension, you can set VS Code as your default script editor
for Godot by following these steps:
1. Open the **Editor Settings**
2. Select **Text Editor > External**
3. Make sure the **Use External Editor** box is checked
4. Fill **Exec Path** with the path to your VS Code executable
5. Fill **Exec Flags** with `{project} --goto {file}:{line}:{col}`
### VS Code
You can use the following settings to configure Godot Tools:
- **GodotTools.godotVersion** - The Godot version of your project.
- **GodotTools.editorPath** - The absolute path to the Godot executable. Required to run the project and test scenes directly from VS Code.
- **GodotTools.workspaceDocumentWithMarkdown** - Control how the documentation of workspace symbols should be rendered: as plain text or as HTML from Markdown.
- **GodotTools.ignoreIndentedVars** - Only parse variables defined on lines without an indentation.
- **GodotTools.parseTextScene** - Parse a file as a Godot scene when the file name ends with `.tscn`.
- **GodotTools.completeNodePath** - Show node paths within a workspace as part of code completion.
- **GodotTools.godotProjectRoot** - Your Godot project's directory, which contains `project.godot` or `engine.cfg`.
## Issues and Contributions
- `editor_path` - The absolute path to the Godot editor executable.
- `gdscript_lsp_server_port` - The WebSocket server port of the GDScript language server.
- `check_status` - Check the GDScript language server connection status.
The [Godot Tools](https://github.com/GodotExplorer/godot-tools) extension and [engine modules](https://github.com/GodotExplorer/editor-server) are both hosted on GitHub. Feel free to open issues there and create pull requests anytime.
## Issues and contributions
See the [full changelog](https://github.com/GodotExplorer/godot-tools/blob/master/CHANGELOG.md) for the latest changes.
The [Godot Tools](https://github.com/godotengine/godot-vscode-plugin) extension
is an open source project from the Godot orgnization. Feel free to open issues
and create pull requests anytime.
See the [full changelog](https://github.com/GodotExplorer/godot-tools/blob/master/CHANGELOG.md)
for the latest changes.
## FAQ
### Why isn't Intellisense showing up for me?
Make sure you save your `.gd` file, then run "GodotTools: Update Workspace Symbols" from the Command Palette.
### Why does it fail to connect to the language server?
## TODO:
* Convert official BBCode documentation into Markdown and render it into HTML with documentation previewer pages
* Add mermaid support with documentation
* Undefined variable checking
- Godot 3.2 or later is required.
- Make sure to open the project in the Godot editor first. If you opened
the editor after opening VS Code, you can click the **Retry** button
in the bottom-right corner in VS Code.
### Why isn't IntelliSense displaying script members?
- GDScript is a dynamically typed script language. The language server can't
infer all variable types.
- To increase the number of results displayed, open the **Editor Settings**,
go to the **Language Server** section then check **Enable Smart Resolve**.

File diff suppressed because one or more lines are too long

View File

@@ -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": "#class_name"},
{ "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": "#pascal_case_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,50 @@
},
"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]+)"
},
"class_name": {
"captures": {
"1": { "name": "entity.name.type.class.gdscript" },
"2": { "name": "class.other.gdscript" }
},
"match": "(?<=class_name)\\s+([a-zA-Z_][a-zA-Z_0-9]*(\\.([a-zA-Z_][a-zA-Z_0-9]*))?)"
},
"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 +263,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 +327,12 @@
]
}
]
},
"pascal_case_class": {
"captures": {
"1": { "name": "entity.name.type.class.gdscript" }
},
"match": "\\b([A-Z][a-zA-Z_0-9]*)\\b"
}
}
}

View File

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

View File

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

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

BIN
img/godot-tools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

2236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,157 +1,117 @@
{
"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": "1.0.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": "npm run compile",
"compile": "tsc -p ./",
"lint": "tslint -p ./",
"watch": "tsc -watch -p ./"
},
"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"
},
{
"command": "godot-tool.list_native_classes",
"title": "Godot Tools: List native classes of godot"
}
],
"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": "Check the gdscript language server connection status"
}
}
},
"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.5.1",
"@types/vscode": "^1.33.0"
},
"dependencies": {
"global": "^4.4.0",
"marked": "^0.7.0",
"vscode-languageclient": "^5.2.1",
"ws": "^7.0.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);

573
src/deps/prism/prism.js Normal file
View File

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

View File

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

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;

167
src/godot-tools.ts Normal file
View File

@@ -0,0 +1,167 @@
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}"`;
if (process.platform === "win32") {
const POWERSHELL = "powershell.exe";
const shell_plugin = vscode.workspace.getConfiguration("terminal.integrated.shell");
let shell = (shell_plugin ? shell_plugin.get("windows", POWERSHELL) : POWERSHELL) || POWERSHELL;
if (shell.endsWith(POWERSHELL)) {
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 the GDScript language server...");
break;
case ClientStatus.CONNECTED:
vscode.window.showInformationMessage("Connected to the GDScript language server.");
break;
case ClientStatus.DISCONNECTED:
this.retry_connect_client();
break;
}
}
private on_client_status_changed(status: ClientStatus) {
switch (status) {
case ClientStatus.PENDING:
this.connection_status.text = `$(sync) Connecting`;
this.connection_status.tooltip = `Connecting to the GDScript language server...`;
break;
case ClientStatus.CONNECTED:
this.connection_status.text = `$(check) Connected`;
this.connection_status.tooltip = `Connected to the 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 = `Disconnected from the GDScript language server.`;
// retry
this.retry_connect_client();
break;
default:
break;
}
}
private retry_connect_client() {
vscode.window.showErrorMessage(`Couldn't connect to the 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
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,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(get_server_uri());
}
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 open workspace is different from 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
View 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;
}
};

198
src/lsp/MessageIO.ts Normal file
View File

@@ -0,0 +1,198 @@
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;
constructor(url: string) {
super();
}
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(url: string):Promise<void> {
return new Promise((resolve, reject) => {
this.socket = null;
const ws = new WebSocket(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);
}
}
}

View File

@@ -0,0 +1,547 @@
import * as vscode from 'vscode';
import { EventEmitter } from "events";
import { MessageIO } from "./MessageIO";
import { NotificationMessage } from "vscode-jsonrpc";
import * as Prism from "../deps/prism/prism";
import * as marked from "marked";
import { Methods, NativeSymbolInspectParams, GodotNativeSymbol, GodotNativeClassInfo, GodotCapabilities } from './gdscript.capabilities';
marked.setOptions({
highlight: function (code, lang) {
return Prism.highlight(code, GDScriptGrammar, lang);
}
});
const enum WebViewMessageType {
INSPECT_NATIVE_SYMBOL = 'INSPECT_NATIVE_SYMBOL',
};
const LIST_NATIVE_CLASS_COMMAND = 'godot-tool.list_native_classes';
export default class NativeDocumentManager extends EventEmitter {
private io: MessageIO = null;
private native_classes: {[key: string]: GodotNativeClassInfo } = {};
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);
} else if (message.method == Methods.GDSCRIPT_CAPABILITIES) {
for (const gdclass of (message.params as GodotCapabilities).native_classes) {
this.native_classes[gdclass.name] = gdclass;
}
for (const gdclass of (message.params as GodotCapabilities).native_classes) {
if (gdclass.inherits) {
const extended_classes = this.native_classes[gdclass.inherits].extended_classes || [];
extended_classes.push(gdclass.name);
this.native_classes[gdclass.inherits].extended_classes = extended_classes;
}
}
}
});
vscode.commands.registerCommand(LIST_NATIVE_CLASS_COMMAND, this.list_native_classes.bind(this));
}
private async list_native_classes() {
let classname = await vscode.window.showQuickPick(
Object.keys(this.native_classes).sort(),
{
placeHolder: 'Type godot class name here',
canPickMany: false
}
);
if (classname) {
this.inspect_native_symbol({native_class: classname, symbol_name: classname});
}
}
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 {
const classlink = make_link(symbol.native_class, undefined);
const classinfo = this.native_classes[symbol.native_class];
function make_function_signature(s: GodotNativeSymbol, with_class = false) {
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} ${with_class?`${classlink}.`:''}${element("a", s.name, {href: `#${s.name}`})}( ${args} )`;
};
function make_symbol_elements(s: GodotNativeSymbol, with_class = false): {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} ${with_class?`${classlink}.`:''}${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} ${with_class?`${classlink}.`:''}${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', `${with_class?`signal ${with_class?`${classlink}.`:''}`:''}${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, with_class);
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) {
let inherits_chian = '';
let base_class = this.native_classes[inherits];
while(base_class) {
inherits_chian += `${inherits_chian?' >':''} ${make_link(base_class.name, undefined)}`;
base_class = this.native_classes[base_class.inherits];
}
inherits = `Inherits: ${inherits_chian}`;
doc += element("p", inherits);
}
if (classinfo && classinfo.extended_classes) {
let inherited = "";
for (const c of classinfo.extended_classes) {
inherited += (inherited ? ', ' : ' ') + make_link(c, c);
}
doc += element("p", `Inherited by:${inherited}`);
}
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);
}
};
doc += element("p", format_documentation(symbol.documentation, symbol.native_class));
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, true);
if (elements.index) {
if ([vscode.SymbolKind.Function, vscode.SymbolKind.Method].indexOf(symbol.kind) == -1) {
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: /![sra](?=[:}]$)/,
alias: 'punctuation'
},
rest: null
}
},
'string': /[\s\S]+/
}
},
'triple-quoted-string': {
pattern: /(?:[rub]|rb|br)?("""|''')[\s\S]+?\1/i,
greedy: true,
alias: 'string'
},
'string': {
pattern: /(?:[rub]|rb|br)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,
greedy: true
},
'function': {
pattern: /((?:^|\s)func[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,
lookbehind: true
},
'class-name': {
pattern: /(\bclass\s+)\w+/i,
lookbehind: true
},
'decorator': {
pattern: /(^\s*)@\w+(?:\.\w+)*/im,
lookbehind: true,
alias: ['annotation', 'punctuation'],
inside: {
'punctuation': /\./
}
},
'keyword': /\b(?:if|elif|else|for|while|break|continue|pass|return|match|func|class|class_name|extends|is|onready|tool|static|export|setget|const|var|as|void|enum|preload|assert|yield|signal|breakpoint|rpc|sync|master|puppet|slave|remotesync|mastersync|puppetsync)\b/,
'builtin': /\b(?:PI|TAU|NAN|INF|_|sin|cos|tan|sinh|cosh|tanh|asin|acos|atan|atan2|sqrt|fmod|fposmod|floor|ceil|round|abs|sign|pow|log|exp|is_nan|is_inf|ease|decimals|stepify|lerp|dectime|randomize|randi|randf|rand_range|seed|rand_seed|deg2rad|rad2deg|linear2db|db2linear|max|min|clamp|nearest_po2|weakref|funcref|convert|typeof|type_exists|char|str|print|printt|prints|printerr|printraw|var2str|str2var|var2bytes|bytes2var|range|load|inst2dict|dict2inst|hash|Color8|print_stack|instance_from_id|preload|yield|assert|Vector2|Vector3|Color|Rect2|Array|Basis|Dictionary|Plane|Quat|RID|Rect3|Transform|Transform2D|AABB|String|Color|NodePath|RID|Object|Dictionary|Array|PoolByteArray|PoolIntArray|PoolRealArray|PoolStringArray|PoolVector2Array|PoolVector3Array|PoolColorArray)\b/,
'boolean': /\b(?:true|false)\b/,
'number': /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
'operator': /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,
'punctuation': /[{}[\];(),.:]/
};
const PrismStyleSheet = `
code[class*="language-"],
pre[class*="language-"] {
color: #657b83; /* base00 */
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
background: #073642; /* base02 */
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
background: #073642; /* base02 */
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background-color: #fdf6e3; /* base3 */
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #93a1a1; /* base1 */
}
.token.punctuation {
color: #586e75; /* base01 */
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #268bd2; /* blue */
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.url,
.token.inserted {
color: #2aa198; /* cyan */
}
.token.entity {
color: #657b83; /* base00 */
background: #eee8d5; /* base2 */
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #859900; /* green */
}
.token.function,
.token.class-name {
color: #b58900; /* yellow */
}
.token.regex,
.token.important,
.token.variable {
color: #cb4b16; /* orange */
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
`;

View File

@@ -0,0 +1,27 @@
import { DocumentSymbol } from "vscode";
export const enum Methods {
GDSCRIPT_CAPABILITIES = 'gdscript/capabilities',
SHOW_NATIVE_SYMBOL = 'gdscript/show_native_symbol',
INSPECT_NATIVE_SYMBOL = 'textDocument/nativeSymbol'
}
export interface NativeSymbolInspectParams {
native_class: string;
symbol_name: string;
}
export class GodotNativeSymbol extends DocumentSymbol {
documentation: string;
native_class: string;
};
export interface GodotNativeClassInfo {
name: string;
inherits: string;
extended_classes?: string[];
}
export interface GodotCapabilities {
native_classes: GodotNativeClassInfo[];
}

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;

15
src/utils.ts Normal file
View 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";
}

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

@@ -1,18 +1,19 @@
{
"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,
"allowJs": true
},
"exclude": [
"node_modules",
"out"
]
}

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