Skip to content

Commit b2d0302

Browse files
committed
feat: Basic keyword completion
1 parent c96d24f commit b2d0302

17 files changed

+727
-83
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/tmp/
22
*.test
3+
gdshader-language-server

.golangci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ linters:
66
- ginkgolinter
77
- gocritic
88
- godot
9+
- goheader
910
- gosec
1011
- govet
1112
- ineffassign
@@ -42,6 +43,8 @@ linters:
4243
enable-all: true
4344
disabled-checks:
4445
- whyNoLint
46+
goheader:
47+
template-path: LICENSE
4548
revive:
4649
enable-all-rules: true
4750
rules:

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ COV_UNIT := $(PWD)/tmp/cover/unit
99
COV_E2E := $(PWD)/tmp/cover/e2e
1010
COV_MERGED := $(PWD)/tmp/cover/merged
1111

12-
reviewable: tidy fmt lint build test
12+
reviewable: tidy fmt lint-fix lint build test
1313

1414
tidy:
1515
go mod tidy
1616

1717
fmt:
1818
golangci-lint fmt
1919

20+
lint-fix:
21+
golangci-lint run --fix
22+
2023
lint:
2124
golangci-lint run
2225

README.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,35 @@ External editor support for `.gdshader` files.
88
> [!WARNING]
99
> 🚧 Early Work in Progress
1010
>
11-
> This project is in its infancy and currently has no features. Feel free to ⭐
12-
> the repo to track progress and signal to me that there is interest!
11+
> This project is in its infancy and currently only supports some basic keyword
12+
> completion. Feel free to ⭐ the repo to track progress and signal to me that
13+
> there is interest!
1314
1415
Godot's shader language is powerful, but editing `.gdshader` files outside the
1516
Godot editor is painful. This project aims to bring proper language tooling
1617
(autocomplete, hover, references, etc.) to editors like Neovim and VSCode.
1718

1819
## 📦 Install
1920

20-
Install from source:
21+
### Neovim
2122

22-
```shell
23-
go install github.com/armsnyder/gdshader-language-server@latest
24-
```
23+
1. Install from source:
2524

26-
## ⚙️ Configure
25+
```shell
26+
go install github.com/armsnyder/gdshader-language-server@latest
27+
```
2728

28-
### Neovim
29+
1. Create a `~/.config/nvim/after/ftplugin/gdshader.lua` file with the following
30+
content, adjusting the path to the `gdshader-language-server` binary if
31+
necessary:
2932

30-
Add the following to your `init.lua`, adjusting the path to the
31-
`gdshader-language-server` binary if necessary:
32-
33-
```lua
34-
vim.api.nvim_create_autocmd("FileType", {
35-
pattern = "gdshader",
36-
callback = function()
37-
vim.lsp.start({
38-
name = "gdshader",
39-
cmd = { vim.fs.expand('$HOME/go/bin/gdshader-language-server') },
40-
})
41-
end,
42-
})
43-
```
33+
```lua
34+
vim.lsp.start({
35+
name = "gdshader",
36+
cmd = { vim.fs.expand('~/go/bin/gdshader-language-server') },
37+
capabilities = vim.lsp.protocol.make_client_capabilities(),
38+
})
39+
```
4440

4541
### VSCode
4642

@@ -50,6 +46,10 @@ Coming soon? Contributions welcome!
5046

5147
Planned features:
5248

49+
- [x] Basic keyword completion
50+
- [ ] Basic shader-type-dependent global built-in completion
51+
(`VERTEX`, `NORMAL`, etc.)
52+
- [ ] More advanced completion (functions, variables, etc.)
5353
- [ ] Go to definition
5454
- [ ] Find references
5555
- [ ] Formatting

e2e_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2025 Adam Snyder
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
123
package main_test
224

325
import (

internal/app/completion.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2025 Adam Snyder
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
package app
24+
25+
import "github.com/armsnyder/gdshader-language-server/internal/lsp"
26+
27+
var completionItems = func() []lsp.CompletionItem {
28+
var items []lsp.CompletionItem
29+
30+
// https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_language.html#data-types
31+
dataTypes := map[string]string{
32+
"void": "Void datatype, useful only for functions that return nothing.",
33+
"bool": "Boolean datatype, can only contain `true` or `false`.",
34+
"bvec2": "Two-component vector of booleans.",
35+
"bvec3": "Three-component vector of booleans.",
36+
"bvec4": "Four-component vector of booleans.",
37+
"int": "32 bit signed scalar integer.",
38+
"ivec2": "Two-component vector of signed integers.",
39+
"ivec3": "Three-component vector of signed integers.",
40+
"ivec4": "Four-component vector of signed integers.",
41+
"uint": "Unsigned scalar integer; can't contain negative numbers.",
42+
"uvec2": "Two-component vector of unsigned integers.",
43+
"uvec3": "Three-component vector of unsigned integers.",
44+
"uvec4": "Four-component vector of unsigned integers.",
45+
"float": "32 bit floating-point scalar.",
46+
"vec2": "Two-component vector of floating-point values.",
47+
"vec3": "Three-component vector of floating-point values.",
48+
"vec4": "Four-component vector of floating-point values.",
49+
"mat2": "2x2 matrix, in column major order.",
50+
"mat3": "3x3 matrix, in column major order.",
51+
"mat4": "4x4 matrix, in column major order.",
52+
"sampler2D": "Sampler type for binding 2D textures, which are read as float.",
53+
"isampler2D": "Sampler type for binding 2D textures, which are read as signed integer.",
54+
"usampler2D": "Sampler type for binding 2D textures, which are read as unsigned integer.",
55+
"sampler2DArray": "Sampler type for binding 2D texture arrays, which are read as float.",
56+
"isampler2DArray": "Sampler type for binding 2D texture arrays, which are read as signed integer.",
57+
"usampler2DArray": "Sampler type for binding 2D texture arrays, which are read as unsigned integer.",
58+
"sampler3D": "Sampler type for binding 3D textures, which are read as float.",
59+
"isampler3D": "Sampler type for binding 3D textures, which are read as signed integer.",
60+
"usampler3D": "Sampler type for binding 3D textures, which are read as unsigned integer.",
61+
"samplerCube": "Sampler type for binding Cubemaps, which are read as float.",
62+
"samplerCubeArray": "Sampler type for binding Cubemap arrays, which are read as float. Only supported in Forward+ and Mobile, not Compatibility.",
63+
"samplerExternalOES": "External sampler type. Only supported in Compatibility/Android platform.",
64+
}
65+
66+
for label, doc := range dataTypes {
67+
items = append(items, lsp.CompletionItem{
68+
Label: label,
69+
Kind: lsp.CompletionClass,
70+
Documentation: &lsp.MarkupContent{Kind: lsp.MarkupMarkdown, Value: doc},
71+
})
72+
}
73+
74+
// TODO(asnyder): Filter keywords based on context.
75+
76+
simpleKeywords := []string{"break", "case", "continue", "default", "do", "else", "for", "if", "return", "switch", "while", "const", "struct"}
77+
78+
for _, keyword := range simpleKeywords {
79+
items = append(items, lsp.CompletionItem{
80+
Label: keyword,
81+
Kind: lsp.CompletionKeyword,
82+
})
83+
}
84+
85+
describedKeywords := map[string]string{
86+
// https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_language.html#precision
87+
"lowp": "low precision, usually 8 bits per component mapped to 0-1",
88+
"mediump": "medium precision, usually 16 bits or half float",
89+
"highp": "high precision, uses full float or integer range (32 bit default)",
90+
"discard": "Discards the current fragment, preventing it from being drawn. Used in fragment shaders to skip rendering under certain conditions.",
91+
"in": "An agument only for reading",
92+
"out": "An argument only for writing",
93+
"inout": "An argument that is fully passed via reference",
94+
"shader_type": "Declares the type of shader being written, such as `canvas_item`, `spatial`, or `particle`.",
95+
"uniform": "Declares a variable that can be set from outside the shader",
96+
"varying": "Declares a variable that is passed between vertex and fragment shaders",
97+
"flat": "The value is not interpolated",
98+
"smooth": "The value is interpolated in a perspective-correct fashion. This is the default.",
99+
"uniform_group": "Group multiple uniforms together in the inspector",
100+
}
101+
102+
for label, doc := range describedKeywords {
103+
items = append(items, lsp.CompletionItem{
104+
Label: label,
105+
Kind: lsp.CompletionKeyword,
106+
Documentation: &lsp.MarkupContent{
107+
Kind: lsp.MarkupMarkdown,
108+
Value: doc,
109+
},
110+
})
111+
}
112+
113+
// Non-function uniform himts.
114+
uniformHints := map[string]string{
115+
"source_color": "Used as color.",
116+
"hint_normal": "Used as normalmap.",
117+
"hint_default_white": "As value or albedo color, default to opaque white.",
118+
"hint_default_black": "As value or albedo color, default to opaque black.",
119+
"hint_default_transparent": "As value or albedo color, default to transparent black.",
120+
"hint_anisotropy": "As flowmap, default to right.",
121+
"repeat_enable": "Enabled texture repeating.",
122+
"repeat_disable": "Disabled texture repeating.",
123+
"hint_screen_texture": "Texture is the screen texture.",
124+
"hint_depth_texture": "Texture is the depth texture.",
125+
"hint_normal_roughness_texture": "Texture is the normal roughness texture (only supported in Forward+).",
126+
}
127+
128+
roughnessHints := []string{"r", "g", "b", "a", "normal", "gray"}
129+
for _, channel := range roughnessHints {
130+
uniformHints["hint_roughness_"+channel] = "Used for roughness limiter on import (attempts reducing specular aliasing). `_normal` is a normal map that guides the roughness limiter, with roughness increasing in areas that have high-frequency detail."
131+
}
132+
133+
filterHints := []string{"nearest", "linear", "nearest_mipmap_nearest", "linear_mipmap_nearest", "nearest_mipmap_linear", "linear_mipmap_linear"}
134+
for _, filter := range filterHints {
135+
uniformHints["hint_filter_"+filter] = "Enabled specified texture filtering."
136+
}
137+
138+
for label, doc := range uniformHints {
139+
items = append(items, lsp.CompletionItem{
140+
Label: label,
141+
Kind: lsp.CompletionKeyword,
142+
Documentation: &lsp.MarkupContent{Kind: lsp.MarkupMarkdown, Value: doc},
143+
})
144+
}
145+
146+
// Function uniform hints.
147+
functionUniformHints := map[string]string{
148+
"hint_enum": "Displays int input as a dropdown widget in the editor.",
149+
"hint_range": "Displays float input as a slider in the editor.",
150+
}
151+
152+
for label, doc := range functionUniformHints {
153+
items = append(items, lsp.CompletionItem{
154+
Label: label,
155+
Kind: lsp.CompletionFunction,
156+
Documentation: &lsp.MarkupContent{Kind: lsp.MarkupMarkdown, Value: doc},
157+
})
158+
}
159+
160+
// Shader types
161+
shaderTypes := map[string]string{
162+
"canvas_item": "Canvas item shader, used for 2D rendering.",
163+
"spatial": "Spatial shader, used for 3D rendering.",
164+
"particles": "Particle shader, used for particle systems.",
165+
"sky": "Sky shader, used for rendering skyboxes or skydomes.",
166+
"fog": "Fog shader, used for rendering fog effects.",
167+
}
168+
for label, doc := range shaderTypes {
169+
items = append(items, lsp.CompletionItem{
170+
Label: label,
171+
Kind: lsp.CompletionKeyword,
172+
Documentation: &lsp.MarkupContent{Kind: lsp.MarkupMarkdown, Value: doc},
173+
})
174+
}
175+
176+
// Built-in variables
177+
// https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_language.html#built-in-variables
178+
// TODO(asnyder): Set variables based on shader type.
179+
180+
return items
181+
}()

0 commit comments

Comments
 (0)