Skip to content

Commit 60e142e

Browse files
authored
Merge pull request #8 from common-workflow-language/sandbox
Implements completion
2 parents 6905630 + 025343d commit 60e142e

File tree

6 files changed

+297
-0
lines changed

6 files changed

+297
-0
lines changed

README.md

+59
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,62 @@
55
# What is Language Server ?
66

77
[Langserver\.org](https://langserver.org/)
8+
9+
# Requirements
10+
11+
python3
12+
13+
# How to setup
14+
15+
```console
16+
git clone -b sandbox https://github.com/common-workflow-language/cwl-language-server.git
17+
cd cwl-language-server
18+
pip install -r requirements.txt
19+
```
20+
21+
## Install pylspclient
22+
23+
Currently, we must use `send-result-wip` branch of [yeger00/pylspclient: LSP client implementation in Python](https://github.com/yeger00/pylspclient/)
24+
25+
# How to execute example
26+
27+
```console
28+
$ PYTHONPATH=/path/to/cwl-language-server python examples/first-step.py
29+
```
30+
31+
## for example
32+
33+
```
34+
PYTHONPATH=$PWD python examples/first-step.py
35+
```
36+
37+
### Expected Result
38+
39+
Looks like this.
40+
41+
```console
42+
$ python examples/first-step.py
43+
{'capabilities': {'completionProvider': {'triggerCharacters': [': ']}}}
44+
None
45+
{'jsonrpc': '2.0', 'method': 'initialized', 'params': {}}
46+
[{'label': 'draft-2', 'deprecated': True}, {'label': 'draft-3.dev1', 'deprecated': True}, {'label': 'draft-3.dev2', 'deprecated': True}, {'label': 'draft-3.dev3', 'deprecated': True}, {'label': 'draft-3.dev4', 'deprecated': True}, {'label': 'draft-3.dev5', 'deprecated': True}, {'label': 'draft-3', 'deprecated': True}, {'label': 'draft-4.dev1', 'deprecated': True}, {'label': 'draft-4.dev2', 'deprecated': True}, {'label': 'draft-4.dev3', 'deprecated': True}, {'label': 'v1.0.dev4', 'deprecated': True}, {'label': 'v1.0'}]
47+
```
48+
49+
Use `CTRL-c` to exit the example.
50+
51+
# Example: Emacs with [Eglot](https://github.com/joaotavora/eglot)
52+
53+
- Install Eglot via package manager such as package.el
54+
- Configure Eglot for cwl-language-server
55+
- Add the following to `init.el`:
56+
```elisp
57+
(require 'eglot)
58+
(add-to-list 'eglot-server-programs
59+
'(cwl-mode . ("/path/to/python" "/path/to/cwl-language-server/cwl_language_server/main.py")))
60+
(add-hook 'cwl-mode-hook 'eglot-ensure)
61+
(eglot-ensure)
62+
```
63+
- Open CWL file
64+
- Set the cursor after `cwlVersion: `
65+
- `M-x completion-at-point`
66+
- Have fun!

cwl_language_server/callbacks.py

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python
2+
import pylspclient.lsp_structs as structs
3+
4+
from glob import glob
5+
from os.path import dirname, basename
6+
import sys
7+
from urllib.parse import urlparse
8+
9+
CONTENTS = {} # Hash[uri, Hash[string, Union[integer, TextContent]]]
10+
11+
class TextContent:
12+
# TODO: it should be tested
13+
def __init__(self, text): # Text -> self
14+
self.text = text.split("\n")
15+
16+
def __getitem__(self, i): # integer -> string
17+
return self.text[i]
18+
19+
def __str__(self): # None -> string
20+
return "\n".join(self.text)
21+
22+
def remove_text(self, rng): # Range -> None
23+
stline = rng['start']['line']
24+
stcol = rng['start']['character']
25+
enline = rng['end']['line']
26+
encol = rng['end']['character']
27+
28+
before = self.text[stline][:stcol]
29+
after = self.text[enline][encol:]
30+
31+
self.text[stline] = before+after
32+
for _ in range(enline-stline):
33+
self.text.pop(stline+1)
34+
35+
def insert_text(self, beg, text): # Position -> Text -> None
36+
stline = beg['line']
37+
stcol = beg['character']
38+
39+
before = self.text[stline][:stcol]
40+
after = self.text[stline][stcol:]
41+
lines = text.split("\n")
42+
43+
self.text[stline] = before+lines[0]
44+
idx = stline+1
45+
for l in lines[1:]:
46+
self.text.insert(idx, l)
47+
idx += 1
48+
self.text[idx-1] += after
49+
50+
def initialize(params): # InitializeParams -> InitializeResult
51+
return {
52+
'capabilities': {
53+
'textDocumentSync': {
54+
'openClose': True,
55+
'change': 2,
56+
},
57+
'completionProvider': {
58+
'triggerCharacters': [': ', '- '],
59+
},
60+
},
61+
}
62+
63+
def initialized(params): # InitializedParams -> None
64+
pass
65+
66+
def didChangeConfiguration(params): # ConfigurationParams -> None
67+
pass
68+
69+
def didOpen(params): # DidOpenTextDocumentParams -> None
70+
uri = params['textDocument']['uri']
71+
text = params['textDocument']['text']
72+
# print('Contents for {} is added.'.format(uri), file=sys.stderr)
73+
CONTENTS[uri] = {
74+
'version': 0,
75+
'text': TextContent(text),
76+
}
77+
78+
def didClose(params): # DidCloseTextDocumentParams -> None
79+
# print('Contents for {} is removed.'.format(params['uri']), file=sys.stderr)
80+
del CONTENTS[params['uri']]
81+
82+
def didChange(params): # DidChangeTextDocumentParams -> None
83+
uri = params['textDocument']['uri']
84+
version = params['textDocument']['version']
85+
changes = params['contentChanges']
86+
87+
if version is None:
88+
pass # warning
89+
elif CONTENTS[uri]['version']+len(changes) != version:
90+
pass # warning
91+
92+
wholetxt = CONTENTS[uri]['text']
93+
for ch in changes:
94+
txt = ch['text']
95+
if not 'range' in ch or ch['range'] is None:
96+
CONTENTS[uri] = {
97+
'text': TextContent(txt),
98+
'version': 0,
99+
}
100+
wholetxt = CONTENTS[uri]['text']
101+
continue
102+
103+
wholetxt.remove_text(ch['range'])
104+
wholetxt.insert_text(ch['range']['start'], txt)
105+
106+
def completion(params): # CompletionParams -> CompletionList
107+
# print('Params: ', params, file=sys.stderr)
108+
ctx = params.get('context', {})
109+
line = params['position']['line']
110+
col = params['position']['character']
111+
uri = params['textDocument']['uri']
112+
113+
# print('Keys: ', CONTENTS.keys(), file=sys.stderr)
114+
field = CONTENTS[uri]['text'][line][0:col].lstrip().rstrip(': ')
115+
if not field:
116+
return structs.CompletionList(False, [])
117+
118+
if field in completion_list:
119+
return completion_list[field]
120+
if field == 'run':
121+
cwls = glob("{}/*.cwl".format(dirname(urlparse(uri).path)))
122+
return [structs.CompletionItem(basename(cwl)) for cwl in cwls if cwl != urlparse(uri).path]
123+
return structs.CompletionList(False, [])
124+
125+
completion_list = {
126+
'cwlVersion': [
127+
structs.CompletionItem('draft-2', deprecated=True),
128+
structs.CompletionItem('draft-3.dev1', deprecated=True),
129+
structs.CompletionItem('draft-3.dev2', deprecated=True),
130+
structs.CompletionItem('draft-3.dev3', deprecated=True),
131+
structs.CompletionItem('draft-3.dev4', deprecated=True),
132+
structs.CompletionItem('draft-3.dev5', deprecated=True),
133+
structs.CompletionItem('draft-3', deprecated=True),
134+
structs.CompletionItem('draft-4.dev1', deprecated=True),
135+
structs.CompletionItem('draft-4.dev2', deprecated=True),
136+
structs.CompletionItem('draft-4.dev3', deprecated=True),
137+
structs.CompletionItem('v1.0.dev4', deprecated=True),
138+
structs.CompletionItem('v1.0'),
139+
],
140+
}
141+
142+
def to_dict(obj):
143+
"""Return a dictionary object for obj.
144+
All the fields with None values are omitted.
145+
It is used for debugging purpose."""
146+
if obj is None:
147+
return obj
148+
if isinstance(obj, list):
149+
return [to_dict(o) for o in obj if o is not None]
150+
if isinstance(obj, dict):
151+
return obj
152+
if isinstance(obj, object):
153+
return { k:v for k, v in obj.__dict__.items() if v is not None }
154+
return obj
155+
156+
if __name__ == '__main__':
157+
pass

cwl_language_server/main.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python
2+
import sys
3+
4+
import pylspclient
5+
6+
import callbacks
7+
8+
def main():
9+
stdin = sys.stdout.buffer
10+
stdout = sys.stdin.buffer
11+
json_rpc_endpoint = pylspclient.JsonRpcEndpoint(stdin, stdout)
12+
lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint,
13+
method_callbacks={
14+
'initialize': callbacks.initialize,
15+
'textDocument/completion': callbacks.completion,
16+
},
17+
notify_callbacks={
18+
'initialized': callbacks.initialized,
19+
'workspace/didChangeConfiguration': callbacks.didChangeConfiguration,
20+
'textDocument/didOpen': callbacks.didOpen,
21+
'textDocument/didChange': callbacks.didChange,
22+
'textDocument/didClose': callbacks.didClose,
23+
})
24+
lsp_endpoint.start()
25+
26+
if __name__ == '__main__':
27+
main()

examples/echo.cwl

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class: CommandLineTool
2+
cwlVersion: v1.0
3+
id: echo_cwl
4+
baseCommand:
5+
- cowsay
6+
inputs:
7+
- id: input
8+
type: string?
9+
inputBinding:
10+
position: 0
11+
label: Input string
12+
doc: This is an input string
13+
outputs:
14+
output:
15+
type: stdout
16+
stdout: output
17+
requirements:
18+
- class: DockerRequirement
19+
dockerPull: docker/whalesay

examples/first-step.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python
2+
import os
3+
import sys
4+
5+
import pylspclient
6+
from pylspclient.lsp_structs import TextDocumentItem, Position, CompletionContext, CompletionTriggerKind, to_type
7+
import cwl_language_server.callbacks as callbacks
8+
9+
def print_response(resp):
10+
print(callbacks.to_dict(resp), file=sys.stderr)
11+
12+
if __name__ == '__main__':
13+
pipein, pipeout = os.pipe()
14+
pipein = os.fdopen(pipein, 'rb')
15+
pipeout = os.fdopen(pipeout, 'wb')
16+
json_rpc_endpoint = pylspclient.JsonRpcEndpoint(pipeout, pipein)
17+
lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint,
18+
method_callbacks={
19+
'initialize': callbacks.initialize,
20+
'initialized': callbacks.initialized,
21+
'textDocument/completion': callbacks.completion,
22+
})
23+
client = pylspclient.LspClient(lsp_endpoint)
24+
print_response(client.initialize(processId=None, rootPath=None, rootUri=None,
25+
initializationOptions=None, capabilities={},
26+
trace=None, workspaceFolders=None))
27+
print_response(client.initialized())
28+
print_response(client.completion(
29+
TextDocumentItem('echo.cwl', 'cwl', 1, ''),
30+
Position(1, 12),
31+
CompletionContext(CompletionTriggerKind.Invoked)))
32+
print_response(client.shutdown())
33+
print_response(client.exit())
34+
# BUG: Need ^C

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
typing-extensions
2+
git+https://github.com/yeger00/pylspclient.git@

0 commit comments

Comments
 (0)