Skip to content

Commit 55c5f53

Browse files
Merge pull request microsoft#41 from pratham1002/pratham1002/gopls
feature: Go Language Support (gopls)
2 parents 35563a7 + 69373e0 commit 55c5f53

File tree

8 files changed

+303
-0
lines changed

8 files changed

+303
-0
lines changed

.github/workflows/publish-to-pypi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v4
17+
- name: Set up Go
18+
uses: actions/setup-go@v5
19+
with:
20+
go-version: '1.21'
21+
- name: Install gopls
22+
run: go install golang.org/x/tools/gopls@latest
1723
- name: Set up Python
1824
uses: actions/setup-python@v5
1925
with:

.github/workflows/test-workflow.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ jobs:
1313

1414
steps:
1515
- uses: actions/checkout@v4
16+
- name: Set up Go
17+
uses: actions/setup-go@v5
18+
with:
19+
go-version: '1.21'
20+
- name: Install gopls
21+
run: go install golang.org/x/tools/gopls@latest
1622
- name: Set up Python
1723
uses: actions/setup-python@v5
1824
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ FodyWeavers.xsd
398398
*.sln.iml
399399

400400
src/multilspy/language_servers/eclipse_jdtls/static/
401+
src/multilspy/language_servers/gopls/static/
401402
src/multilspy/language_servers/omnisharp/static/
402403
src/multilspy/language_servers/rust_analyzer/static/
403404
src/multilspy/language_servers/typescript_language_server/static/

src/multilspy/language_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo
100100
TypeScriptLanguageServer,
101101
)
102102
return TypeScriptLanguageServer(config, logger, repository_root_path)
103+
elif config.code_language == Language.GO:
104+
from multilspy.language_servers.gopls.gopls import Gopls
105+
106+
return Gopls(config, logger, repository_root_path)
103107
else:
104108
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
105109
raise MultilspyException(f"Language {config.code_language} is not supported")
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import asyncio
2+
import json
3+
import logging
4+
import os
5+
import pathlib
6+
import subprocess
7+
from contextlib import asynccontextmanager
8+
from typing import AsyncIterator
9+
10+
from multilspy.multilspy_logger import MultilspyLogger
11+
from multilspy.language_server import LanguageServer
12+
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
13+
from multilspy.lsp_protocol_handler.lsp_types import InitializeParams
14+
from multilspy.multilspy_config import MultilspyConfig
15+
16+
17+
class Gopls(LanguageServer):
18+
"""
19+
Provides Go specific instantiation of the LanguageServer class using gopls.
20+
"""
21+
22+
@staticmethod
23+
def _get_go_version():
24+
"""Get the installed Go version or None if not found."""
25+
try:
26+
result = subprocess.run(['go', 'version'], capture_output=True, text=True)
27+
if result.returncode == 0:
28+
return result.stdout.strip()
29+
except FileNotFoundError:
30+
return None
31+
return None
32+
33+
@staticmethod
34+
def _get_gopls_version():
35+
"""Get the installed gopls version or None if not found."""
36+
try:
37+
result = subprocess.run(['gopls', 'version'], capture_output=True, text=True)
38+
if result.returncode == 0:
39+
return result.stdout.strip()
40+
except FileNotFoundError:
41+
return None
42+
return None
43+
44+
@classmethod
45+
def setup_runtime_dependency(cls):
46+
"""
47+
Check if required Go runtime dependencies are available.
48+
Raises RuntimeError with helpful message if dependencies are missing.
49+
"""
50+
missing_deps = []
51+
52+
# Check for Go installation
53+
go_version = cls._get_go_version()
54+
if not go_version:
55+
missing_deps.append(("Go", "https://golang.org/doc/install"))
56+
57+
# Check for gopls
58+
gopls_version = cls._get_gopls_version()
59+
if not gopls_version:
60+
missing_deps.append(("gopls", "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme"))
61+
62+
if missing_deps:
63+
error_msg = "Missing required dependencies:\n"
64+
for dep, install_url in missing_deps:
65+
error_msg += f"- {dep}: Please install from {install_url}\n"
66+
raise RuntimeError(error_msg)
67+
68+
return True
69+
70+
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
71+
# Check runtime dependencies before initializing
72+
self.setup_runtime_dependency()
73+
74+
super().__init__(
75+
config,
76+
logger,
77+
repository_root_path,
78+
ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path),
79+
"go",
80+
)
81+
self.server_ready = asyncio.Event()
82+
self.request_id = 0
83+
84+
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
85+
"""
86+
Returns the initialize params for the TypeScript Language Server.
87+
"""
88+
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f:
89+
d = json.load(f)
90+
91+
del d["_description"]
92+
93+
d["processId"] = os.getpid()
94+
assert d["rootPath"] == "$rootPath"
95+
d["rootPath"] = repository_absolute_path
96+
97+
assert d["rootUri"] == "$rootUri"
98+
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
99+
100+
assert d["workspaceFolders"][0]["uri"] == "$uri"
101+
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
102+
103+
assert d["workspaceFolders"][0]["name"] == "$name"
104+
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
105+
106+
return d
107+
108+
@asynccontextmanager
109+
async def start_server(self) -> AsyncIterator["Gopls"]:
110+
"""Start gopls server process"""
111+
async def register_capability_handler(params):
112+
return
113+
114+
async def window_log_message(msg):
115+
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
116+
117+
async def do_nothing(params):
118+
return
119+
120+
self.server.on_request("client/registerCapability", register_capability_handler)
121+
self.server.on_notification("window/logMessage", window_log_message)
122+
self.server.on_notification("$/progress", do_nothing)
123+
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
124+
125+
async with super().start_server():
126+
self.logger.log("Starting gopls server process", logging.INFO)
127+
await self.server.start()
128+
initialize_params = self._get_initialize_params(self.repository_root_path)
129+
130+
self.logger.log(
131+
"Sending initialize request from LSP client to LSP server and awaiting response",
132+
logging.INFO,
133+
)
134+
init_response = await self.server.send.initialize(initialize_params)
135+
136+
# Verify server capabilities
137+
assert "textDocumentSync" in init_response["capabilities"]
138+
assert "completionProvider" in init_response["capabilities"]
139+
assert "definitionProvider" in init_response["capabilities"]
140+
141+
self.server.notify.initialized({})
142+
self.completions_available.set()
143+
144+
# gopls server is typically ready immediately after initialization
145+
self.server_ready.set()
146+
await self.server_ready.wait()
147+
148+
yield self
149+
150+
await self.server.shutdown()
151+
await self.server.stop()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
3+
"processId": "os.getpid()",
4+
"locale": "en",
5+
"rootPath": "$rootPath",
6+
"rootUri": "$rootUri",
7+
"capabilities": {
8+
"textDocument": {
9+
"synchronization": {
10+
"didSave": true,
11+
"dynamicRegistration": true
12+
},
13+
"completion": {
14+
"dynamicRegistration": true,
15+
"completionItem": {
16+
"snippetSupport": true
17+
}
18+
},
19+
"definition": {
20+
"dynamicRegistration": true
21+
}
22+
},
23+
"workspace": {
24+
"workspaceFolders": true,
25+
"didChangeConfiguration": {
26+
"dynamicRegistration": true
27+
}
28+
}
29+
},
30+
"workspaceFolders": [
31+
{
32+
"uri": "$uri",
33+
"name": "$name"
34+
}
35+
]
36+
}

src/multilspy/multilspy_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Language(str, Enum):
1616
JAVA = "java"
1717
TYPESCRIPT = "typescript"
1818
JAVASCRIPT = "javascript"
19+
GO = "go"
1920

2021
def __str__(self) -> str:
2122
return self.value

tests/multilspy/test_multilspy_go.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
This file contains tests for running the Golang Language Server: gopls
3+
"""
4+
5+
import pytest
6+
from multilspy import LanguageServer
7+
from multilspy.multilspy_config import Language
8+
from tests.test_utils import create_test_context
9+
from pathlib import PurePath
10+
11+
pytest_plugins = ("pytest_asyncio",)
12+
13+
@pytest.mark.asyncio
14+
async def test_multilspy_golang_example():
15+
"""
16+
Test the working of multilspy with golang repository - https://github.com/golang/example
17+
"""
18+
code_language = Language.GO
19+
params = {
20+
"code_language": code_language,
21+
"repo_url": "https://github.com/golang/example/",
22+
"repo_commit": "1bcfdd08c5584d89507e37be70af63c72eb8c16f"
23+
}
24+
with create_test_context(params) as context:
25+
lsp = LanguageServer.create(context.config, context.logger, context.source_directory)
26+
27+
async with lsp.start_server():
28+
# Wait for server to be fully initialized
29+
await lsp.server_ready.wait()
30+
31+
path = str(PurePath("hello/hello.go"))
32+
33+
# Test 1: Get definition of the 'reverse' package import
34+
result = await lsp.request_definition(path, 29, 8)
35+
assert isinstance(result, list)
36+
assert len(result) == 1
37+
38+
item = result[0]
39+
assert "reverse" in item["relativePath"]
40+
assert item["range"]["start"]["line"] == 5 # Package declaration is after license header
41+
42+
# Test 2: Find references to the 'name' variable
43+
result = await lsp.request_references(path, 61, 5) # Position of name declaration
44+
assert isinstance(result, list)
45+
assert len(result) == 5
46+
47+
for item in result:
48+
del item["uri"]
49+
del item["absolutePath"]
50+
51+
assert result == [
52+
{
53+
'range': {
54+
'start': {'line': 59, 'character': 2},
55+
'end': {'line': 59, 'character': 6}
56+
},
57+
'relativePath': 'hello/hello.go'
58+
},
59+
{
60+
'range': {
61+
'start': {'line': 61, 'character': 4},
62+
'end': {'line': 61, 'character': 8}
63+
},
64+
'relativePath': 'hello/hello.go'
65+
},
66+
{
67+
'range': {
68+
'start': {'line': 62, 'character': 32},
69+
'end': {'line': 62, 'character': 36}
70+
},
71+
'relativePath': 'hello/hello.go'
72+
},
73+
{
74+
'range': {
75+
'start': {'line': 67, 'character': 68},
76+
'end': {'line': 67, 'character': 72}
77+
},
78+
'relativePath': 'hello/hello.go'
79+
},
80+
{
81+
'range': {
82+
'start': {'line': 70, 'character': 36},
83+
'end': {'line': 70, 'character': 40}
84+
},
85+
'relativePath': 'hello/hello.go'
86+
}
87+
]
88+
89+
# Test 3: Get hover information for the 'name' variable
90+
result = await lsp.request_hover(path, 59, 5) # Position of reverse.String call
91+
assert result is not None
92+
assert "var name string" in result["contents"]["value"]
93+
# Test 4: Get document symbols
94+
result = await lsp.request_document_symbols(path)
95+
assert isinstance(result, tuple)
96+
97+
# Check if the symbols are in the result
98+
assert [symbol["name"] for symbol in result[0]] == ["usage", "greeting", "reverseFlag", "main"]

0 commit comments

Comments
 (0)