Skip to content

Commit e57574f

Browse files
committed
feat: add gopls (Go language server) support
- Add GoplsClient with full LSP capabilities (hover, definition, references, type definition, implementation, call hierarchy, type hierarchy, document symbols, workspace symbols) - Include gopls-specific initialization options and configuration - Add automatic gopls installation via 'go install golang.org/x/tools/gopls@latest' - Add container support with Go 1.23 Alpine image - Register gopls in container/registry.toml for automatic version tracking - Export GoplsClient and GoClient alias in clients module
1 parent 78989a9 commit e57574f

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

container/gopls/ContainerFile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
ARG VERSION=latest
2+
FROM docker.io/library/golang:1.23-alpine AS builder
3+
ARG VERSION
4+
WORKDIR /app
5+
RUN apk add --no-cache git && \
6+
go install golang.org/x/tools/gopls@${VERSION}
7+
8+
FROM docker.io/library/golang:1.23-alpine
9+
ARG VERSION
10+
LABEL org.opencontainers.image.title="Gopls LSP" \
11+
org.opencontainers.image.description="Go Language Server" \
12+
org.opencontainers.image.version="${VERSION}" \
13+
org.opencontainers.image.source="https://github.com/wangbowei/lsp-client" \
14+
org.opencontainers.image.licenses="MIT"
15+
16+
WORKDIR /app
17+
COPY --from=builder /go/bin/gopls /usr/local/bin/gopls
18+
19+
WORKDIR /workspace
20+
ENTRYPOINT ["gopls", "serve"]

container/registry.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ type = "github"
2424
repo = "denoland/deno"
2525
strip_v = true
2626

27+
[gopls]
28+
type = "github"
29+
repo = "golang/tools"
30+
2731
# [custom-server]
2832
# type = "custom"
2933
# command = "curl -s https://api.example.com/version | jq -r .version"

src/lsp_client/clients/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from __future__ import annotations
22

3+
from .gopls import GoplsClient
34
from .pyrefly import PyreflyClient
45
from .pyright import PyrightClient
56
from .rust_analyzer import RustAnalyzerClient
67
from .typescript import TypescriptClient
78

9+
GoClient = GoplsClient
810
PythonClient = PyrightClient
911
RustClient = RustAnalyzerClient
1012
TypeScriptClient = TypescriptClient
1113

1214
__all__ = [
15+
"GoClient",
16+
"GoplsClient",
1317
"PyreflyClient",
1418
"PythonClient",
1519
"RustClient",

src/lsp_client/clients/gopls.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
from __future__ import annotations
2+
3+
import shutil
4+
from functools import partial
5+
from subprocess import CalledProcessError
6+
from typing import Any, Literal, override
7+
8+
import anyio
9+
from attrs import define, field
10+
from loguru import logger
11+
12+
from lsp_client.capability.notification import (
13+
WithNotifyDidChangeConfiguration,
14+
)
15+
from lsp_client.capability.request import (
16+
WithRequestCallHierarchy,
17+
WithRequestDefinition,
18+
WithRequestDocumentSymbol,
19+
WithRequestHover,
20+
WithRequestImplementation,
21+
WithRequestReferences,
22+
WithRequestTypeDefinition,
23+
WithRequestTypeHierarchy,
24+
WithRequestWorkspaceSymbol,
25+
)
26+
from lsp_client.capability.server_notification import (
27+
WithReceiveLogMessage,
28+
WithReceiveLogTrace,
29+
WithReceivePublishDiagnostics,
30+
WithReceiveShowMessage,
31+
)
32+
from lsp_client.capability.server_request import (
33+
WithRespondConfigurationRequest,
34+
WithRespondShowDocumentRequest,
35+
WithRespondShowMessageRequest,
36+
WithRespondWorkspaceFoldersRequest,
37+
)
38+
from lsp_client.client.abc import Client
39+
from lsp_client.server import DefaultServers, ServerInstallationError
40+
from lsp_client.server.container import ContainerServer
41+
from lsp_client.server.local import LocalServer
42+
from lsp_client.utils.types import lsp_type
43+
44+
GoplsContainerServer = partial(
45+
ContainerServer, image="ghcr.io/observerw/lsp-client/gopls:latest"
46+
)
47+
48+
49+
async def ensure_gopls_installed() -> None:
50+
if shutil.which("gopls"):
51+
return
52+
53+
logger.warning("gopls not found, attempting to install via go install...")
54+
55+
try:
56+
await anyio.run_process(["go", "install", "golang.org/x/tools/gopls@latest"])
57+
logger.info("Successfully installed gopls via go install")
58+
return
59+
except CalledProcessError as e:
60+
raise ServerInstallationError(
61+
"Could not install gopls. Please install it manually with 'go install golang.org/x/tools/gopls@latest'. "
62+
"See https://github.com/golang/tools/tree/master/gopls for more information."
63+
) from e
64+
65+
66+
GoplsLocalServer = partial(
67+
LocalServer,
68+
program="gopls",
69+
args=["serve"],
70+
ensure_installed=ensure_gopls_installed,
71+
)
72+
73+
74+
@define
75+
class GoplsClient(
76+
Client,
77+
WithNotifyDidChangeConfiguration,
78+
WithRequestCallHierarchy,
79+
WithRequestDefinition,
80+
WithRequestDocumentSymbol,
81+
WithRequestHover,
82+
WithRequestImplementation,
83+
WithRequestReferences,
84+
WithRequestTypeDefinition,
85+
WithRequestTypeHierarchy,
86+
WithRequestWorkspaceSymbol,
87+
WithReceiveLogMessage,
88+
WithReceiveLogTrace,
89+
WithReceivePublishDiagnostics,
90+
WithReceiveShowMessage,
91+
WithRespondConfigurationRequest,
92+
WithRespondShowDocumentRequest,
93+
WithRespondShowMessageRequest,
94+
WithRespondWorkspaceFoldersRequest,
95+
):
96+
"""
97+
- Language: Go
98+
- Homepage: https://pkg.go.dev/golang.org/x/tools/gopls
99+
- Doc: https://github.com/golang/tools/blob/master/gopls/README.md
100+
- Github: https://github.com/golang/tools/tree/master/gopls
101+
- VSCode Extension: https://marketplace.visualstudio.com/items?itemName=golang.go
102+
"""
103+
104+
all_experiments: bool = False
105+
analyses: dict[str, bool | str] = field(factory=dict)
106+
allow_modfile_mods: bool = False
107+
allow_multi_line_string_literals: bool = False
108+
allow_implicit_variable_assignments: bool = False
109+
build_directory: str | None = None
110+
codelenses: dict[str, bool] = field(factory=dict)
111+
complete_completions: bool = False
112+
complete_unimported: bool = False
113+
completion_budget: str = "500ms"
114+
diagnostics_delay: str = "500ms"
115+
documentation_options: dict[str, bool] = field(factory=dict)
116+
experimental_postfix_completions: bool = True
117+
experimental_prefixed_format: bool = True
118+
experimental_template_support: bool = False
119+
experimental_workspace_module: bool = False
120+
gofumpt: bool = False
121+
hover_kind: Literal[
122+
"FullDocumentation", "NoDocumentation", "SingleLine", "Structured"
123+
] = "FullDocumentation"
124+
link_in_hover: bool = True
125+
link_target: str = "pkg.go.dev"
126+
matcher: Literal["Fuzzy", "CaseInsensitive", "CaseSensitive"] = "Fuzzy"
127+
semantic_tokens: bool = True
128+
staticcheck: bool = False
129+
use_placeholders: bool = False
130+
verbose_output: bool = False
131+
132+
@override
133+
def get_language_id(self) -> lsp_type.LanguageKind:
134+
return lsp_type.LanguageKind.Go
135+
136+
@override
137+
def create_default_servers(self) -> DefaultServers:
138+
return DefaultServers(
139+
local=GoplsLocalServer(),
140+
container=GoplsContainerServer(),
141+
)
142+
143+
@override
144+
def create_initialization_options(self) -> dict[str, Any]:
145+
options: dict[str, Any] = {}
146+
147+
if self.all_experiments:
148+
options["allExperiments"] = self.all_experiments
149+
150+
if self.analyses:
151+
options["analyses"] = self.analyses
152+
153+
if self.allow_modfile_mods:
154+
options["allowModfileMods"] = self.allow_modfile_mods
155+
156+
if self.allow_multi_line_string_literals:
157+
options["allowMultiLineStringLiterals"] = (
158+
self.allow_multi_line_string_literals
159+
)
160+
161+
if self.allow_implicit_variable_assignments:
162+
options["allowImplicitVariableAssignments"] = (
163+
self.allow_implicit_variable_assignments
164+
)
165+
166+
if self.build_directory:
167+
options["buildDirectory"] = self.build_directory
168+
169+
if self.codelenses:
170+
options["codelenses"] = self.codelenses
171+
172+
if self.complete_completions:
173+
options["completeCompletions"] = self.complete_completions
174+
175+
if self.complete_unimported:
176+
options["completeUnimported"] = self.complete_unimported
177+
178+
if self.completion_budget:
179+
options["completionBudget"] = self.completion_budget
180+
181+
if self.diagnostics_delay:
182+
options["diagnosticsDelay"] = self.diagnostics_delay
183+
184+
if self.documentation_options:
185+
options["documentationOptions"] = self.documentation_options
186+
187+
options["experimentalPostfixCompletions"] = (
188+
self.experimental_postfix_completions
189+
)
190+
options["experimentalPrefixedFormat"] = self.experimental_prefixed_format
191+
options["experimentalTemplateSupport"] = self.experimental_template_support
192+
options["experimentalWorkspaceModule"] = self.experimental_workspace_module
193+
options["gofumpt"] = self.gofumpt
194+
options["hoverKind"] = self.hover_kind
195+
options["linkInHover"] = self.link_in_hover
196+
options["linkTarget"] = self.link_target
197+
options["matcher"] = self.matcher
198+
options["semanticTokens"] = self.semantic_tokens
199+
options["staticcheck"] = self.staticcheck
200+
options["usePlaceholders"] = self.use_placeholders
201+
options["verboseOutput"] = self.verbose_output
202+
203+
return options
204+
205+
@override
206+
def check_server_compatibility(self, info: lsp_type.ServerInfo | None) -> None:
207+
return

0 commit comments

Comments
 (0)