Skip to content

Commit f111e6b

Browse files
authored
Merge pull request #477 from krassowski/fix-truncated-stream
Fix truncated stream
2 parents 436ba96 + 4c9bce5 commit f111e6b

File tree

5 files changed

+63
-11
lines changed

5 files changed

+63
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222

2323
- send periodic pings on websocket channels to maintain connection ([#459], thanks @franckchen)
2424
- R languageserver is no longer incorrectly shown as available when not installed ([#463])
25+
- fix completion of very large namespaces (e.g. in R's base or in JavaScript) due to truncated message relay ([#477])
2526

2627
[#459]: https://github.com/krassowski/jupyterlab-lsp/pull/459
2728
[#463]: https://github.com/krassowski/jupyterlab-lsp/pull/463
29+
[#477]: https://github.com/krassowski/jupyterlab-lsp/pull/477
2830

2931
### `@krassowski/jupyterlab-lsp 3.0.0` (2021-01-06)
3032

atest/05_Features/Completion.robot

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ Completes Correctly With R Double And Triple Colon
203203
Wait Until Keyword Succeeds 40x 0.5s File Editor Line Should Equal 3 datasets:::.packageName
204204
[Teardown] Clean Up After Working With File completion.R
205205

206+
Completes Large Namespaces
207+
[Setup] Prepare File for Editing R completion completion.R
208+
Place Cursor In File Editor At 6 7
209+
Wait Until Fully Initialized
210+
Trigger Completer
211+
Completer Should Suggest abs timeout=30s
212+
[Teardown] Clean Up After Working With File completion.R
213+
206214
*** Keywords ***
207215
Setup Completion Test
208216
Setup Notebook Python Completion.ipynb
@@ -235,8 +243,8 @@ Select Completer Suggestion
235243
Click Element ${suggestion} code
236244

237245
Completer Should Suggest
238-
[Arguments] ${text}
239-
Wait Until Page Contains Element ${COMPLETER_BOX} .jp-Completer-item[data-value="${text}"] timeout=10s
246+
[Arguments] ${text} ${timeout}=10s
247+
Wait Until Page Contains Element ${COMPLETER_BOX} .jp-Completer-item[data-value="${text}"] timeout=${timeout}
240248
Capture Page Screenshot ${text.replace(' ', '_')}.png
241249

242250
Completer Should Include Icon

atest/examples/completion.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
tools::
33
# `datasets:::<tab>` → select `.packageName` → `datasets:::.packageName`
44
datasets:::
5+
# `base:::<tab>` → works
6+
base:::

docs/Configuring.ipynb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
},
211211
{
212212
"cell_type": "markdown",
213+
"metadata": {
214+
"collapsed": false
215+
},
213216
"source": [
214217
"### Example: Scala Language Server (metals) integration with jupyterlab-lsp\n",
215218
"\n",
@@ -225,11 +228,13 @@
225228
"$ ./coursier launch --fork almond -- --install\n",
226229
"$ rm -f coursier\n",
227230
"```\n",
231+
"\n",
228232
"Spark Magic kernel:\n",
229233
"\n",
230234
"```bash\n",
231235
"pip install sparkmagic\n",
232236
"```\n",
237+
"\n",
233238
"Now, install the spark kernel:\n",
234239
"\n",
235240
"```bash\n",
@@ -247,7 +252,8 @@
247252
"\n",
248253
"(Might need to use the --force-fetch flag if you are getting dependency issues.)\n",
249254
"\n",
250-
"Step 3: Configure the metals server in jupyterlab-lsp. Enter the following in the jupyter_server_config.json:\n",
255+
"Step 3: Configure the metals server in jupyterlab-lsp. Enter the following in\n",
256+
"the jupyter_server_config.json:\n",
251257
"\n",
252258
"```python\n",
253259
"{\n",
@@ -264,11 +270,10 @@
264270
"}\n",
265271
"```\n",
266272
"\n",
267-
"You are good to go now! Just start `jupyter lab` and create a notebook with either the Spark or the Scala kernel and you should be able to see the metals server initialised from the bottom left corner."
268-
],
269-
"metadata": {
270-
"collapsed": false
271-
}
273+
"You are good to go now! Just start `jupyter lab` and create a notebook with\n",
274+
"either the Spark or the Scala kernel and you should be able to see the metals\n",
275+
"server initialised from the bottom left corner."
276+
]
272277
}
273278
],
274279
"metadata": {

python_packages/jupyter_lsp/jupyter_lsp/stdio.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import io
1313
import os
1414
from concurrent.futures import ThreadPoolExecutor
15-
from typing import Text
15+
from typing import List, Optional, Text
1616

1717
from tornado.concurrent import run_on_executor
1818
from tornado.gen import convert_yielded
@@ -30,7 +30,9 @@ class LspStdIoBase(LoggingConfigurable):
3030

3131
executor = None
3232

33-
stream = Instance(io.BufferedIOBase, help="the stream to read/write")
33+
stream = Instance(
34+
io.BufferedIOBase, help="the stream to read/write"
35+
) # type: io.BufferedIOBase
3436
queue = Instance(Queue, help="queue to get/put")
3537

3638
def __repr__(self): # pragma: no cover
@@ -95,6 +97,39 @@ async def read(self) -> None:
9597
self.log.exception("%s couldn't enqueue message: %s", self, message)
9698
await self.sleep()
9799

100+
def _read_content(self, length: int, max_parts=1000) -> Optional[bytes]:
101+
"""Read the full length of the message unless exceeding max_parts.
102+
103+
See https://github.com/krassowski/jupyterlab-lsp/issues/450
104+
105+
Crucial docs or read():
106+
"If the argument is positive, and the underlying raw
107+
stream is not interactive, multiple raw reads may be issued
108+
to satisfy the byte count (unless EOF is reached first)"
109+
110+
Args:
111+
- length: the content length
112+
- max_parts: prevent absurdly long messages (1000 parts is several MBs):
113+
1 part is usually sufficent but not enough for some long
114+
messages 2 or 3 parts are often needed.
115+
"""
116+
raw_parts: List[bytes] = []
117+
received_size = 0
118+
while received_size < length and len(raw_parts) < max_parts:
119+
part = self.stream.read(length)
120+
if part is None:
121+
break # pragma: no cover
122+
received_size += len(part)
123+
raw_parts.append(part)
124+
125+
if raw_parts:
126+
raw = b"".join(raw_parts)
127+
if len(raw) != length: # pragma: no cover
128+
self.log.warning(
129+
f"Readout and content-length mismatch:" f" {len(raw)} vs {length}"
130+
)
131+
return raw
132+
98133
async def read_one(self) -> Text:
99134
"""Read a single message"""
100135
message = ""
@@ -114,7 +149,7 @@ async def read_one(self) -> Text:
114149
retries = 5
115150
while raw is None and retries:
116151
try:
117-
raw = self.stream.read(content_length)
152+
raw = self._read_content(length=content_length)
118153
except OSError: # pragma: no cover
119154
raw = None
120155
if raw is None: # pragma: no cover

0 commit comments

Comments
 (0)