Skip to content

Commit b97942d

Browse files
joyceerhlrebornix
andauthored
Add language server support for native interactive window (microsoft#16560)
* Extract ConcatTextDocument * Improve text len cache * 💄 * find input document in iw. * hook up with jedi middleware * 💄 * postion is zero based * isClosed should include input document * Dispose NotebookConcatDocument * Properly handle onDidClose * hide diagnostics for interactive cells * unnecessary diagnostics filtering. * class per file. * update styles. * Update unit tests * Try to fix code quality * fix lint/format * remove unary operator * Fix lint * Fix lint * Run prettier * fix input document init. * 💄 * filter out non-python code. * Format * Format Co-authored-by: rebornix <penn.lv@gmail.com>
1 parent fe85613 commit b97942d

11 files changed

Lines changed: 424 additions & 37 deletions

File tree

src/client/activation/languageClientMiddleware.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export class LanguageClientMiddleware implements Middleware {
140140
getClient,
141141
fileSystem,
142142
PYTHON_LANGUAGE,
143-
/.*\.ipynb/m,
143+
/.*\.(ipynb|interactive)/m,
144144
);
145145
}
146146
disposables.push(
@@ -154,7 +154,7 @@ export class LanguageClientMiddleware implements Middleware {
154154
getClient,
155155
fileSystem,
156156
PYTHON_LANGUAGE,
157-
/.*\.ipynb/m,
157+
/.*\.(ipynb|interactive)/m,
158158
);
159159
}
160160
}

src/client/common/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ export const PYTHON_LANGUAGE = 'python';
44
export const PYTHON_WARNINGS = 'PYTHONWARNINGS';
55

66
export const NotebookCellScheme = 'vscode-notebook-cell';
7+
export const InteractiveInputScheme = 'vscode-interactive-input';
8+
export const InteractiveScheme = 'vscode-interactive';
79
export const PYTHON = [
810
{ scheme: 'file', language: PYTHON_LANGUAGE },
911
{ scheme: 'untitled', language: PYTHON_LANGUAGE },
1012
{ scheme: 'vscode-notebook', language: PYTHON_LANGUAGE },
1113
{ scheme: NotebookCellScheme, language: PYTHON_LANGUAGE },
14+
{ scheme: InteractiveInputScheme, language: PYTHON_LANGUAGE },
1215
];
1316

1417
export const PVSC_EXTENSION_ID = 'ms-python.python';

src/client/common/utils/misc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33
'use strict';
44
import type { TextDocument, Uri } from 'vscode';
5-
import { NotebookCellScheme } from '../constants';
5+
import { InteractiveInputScheme, NotebookCellScheme } from '../constants';
66
import { InterpreterUri } from '../installer/types';
77
import { Resource } from '../types';
88
import { isPromise } from './async';
@@ -145,5 +145,5 @@ export function getURIFilter(
145145

146146
export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean {
147147
const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri;
148-
return uri.scheme.includes(NotebookCellScheme);
148+
return uri.scheme.includes(NotebookCellScheme) || uri.scheme.includes(InteractiveInputScheme);
149149
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Position, Range, Uri, Event, Location, TextLine } from 'vscode';
5+
6+
export interface IConcatTextDocument {
7+
onDidChange: Event<void>;
8+
isClosed: boolean;
9+
getText(range?: Range): string;
10+
contains(uri: Uri): boolean;
11+
offsetAt(position: Position): number;
12+
positionAt(locationOrOffset: Location | number): Position;
13+
validateRange(range: Range): Range;
14+
validatePosition(position: Position): Position;
15+
locationAt(positionOrRange: Position | Range): Location;
16+
lineAt(posOrNumber: Position | number): TextLine;
17+
getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined;
18+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
/* eslint-disable class-methods-use-this */
4+
5+
import {
6+
NotebookDocument,
7+
Position,
8+
Range,
9+
TextDocument,
10+
Uri,
11+
workspace,
12+
DocumentSelector,
13+
Event,
14+
EventEmitter,
15+
Location,
16+
TextLine,
17+
} from 'vscode';
18+
import { NotebookConcatTextDocument } from 'vscode-proposed';
19+
20+
import { IVSCodeNotebook } from '../../common/application/types';
21+
import { InteractiveInputScheme, PYTHON_LANGUAGE } from '../../common/constants';
22+
import { IConcatTextDocument } from './concatTextDocument';
23+
24+
export class InteractiveConcatTextDocument implements IConcatTextDocument {
25+
private _input: TextDocument | undefined = undefined;
26+
27+
private _concatTextDocument: NotebookConcatTextDocument;
28+
29+
private _lineCounts: [number, number] = [0, 0];
30+
31+
private _textLen: [number, number] = [0, 0];
32+
33+
private _onDidChange = new EventEmitter<void>();
34+
35+
onDidChange: Event<void> = this._onDidChange.event;
36+
37+
get isClosed(): boolean {
38+
return this._concatTextDocument.isClosed || !!this._input?.isClosed;
39+
}
40+
41+
constructor(
42+
private _notebook: NotebookDocument,
43+
private _selector: DocumentSelector,
44+
notebookApi: IVSCodeNotebook,
45+
) {
46+
this._concatTextDocument = notebookApi.createConcatTextDocument(_notebook, this._selector);
47+
48+
this._concatTextDocument.onDidChange(() => {
49+
// not performant, NotebookConcatTextDocument should provide lineCount
50+
this._updateConcat();
51+
this._onDidChange.fire();
52+
});
53+
54+
workspace.onDidChangeTextDocument((e) => {
55+
if (e.document === this._input) {
56+
this._updateInput();
57+
this._onDidChange.fire();
58+
}
59+
});
60+
61+
this._updateConcat();
62+
this._updateInput();
63+
64+
const counter = /Interactive-(\d+)\.interactive/.exec(this._notebook.uri.path);
65+
if (counter) {
66+
this._input = workspace.textDocuments.find(
67+
(document) => document.uri.path.indexOf(`InteractiveInput-${counter[1]}`) >= 0,
68+
);
69+
}
70+
71+
if (!this._input) {
72+
const once = workspace.onDidOpenTextDocument((e) => {
73+
if (e.uri.scheme === InteractiveInputScheme) {
74+
if (!counter || !counter[1]) {
75+
return;
76+
}
77+
78+
if (e.uri.path.indexOf(`InteractiveInput-${counter[1]}`) >= 0) {
79+
this._input = e;
80+
this._updateInput();
81+
once.dispose();
82+
}
83+
}
84+
});
85+
}
86+
}
87+
88+
private _updateConcat() {
89+
let concatLineCnt = 0;
90+
let concatTextLen = 0;
91+
for (let i = 0; i < this._notebook.cellCount; i += 1) {
92+
const cell = this._notebook.cellAt(i);
93+
if (cell.document.languageId === PYTHON_LANGUAGE) {
94+
concatLineCnt += cell.document.lineCount + 1;
95+
concatTextLen += this._getDocumentTextLen(cell.document) + 1;
96+
}
97+
}
98+
99+
this._lineCounts = [
100+
concatLineCnt > 0 ? concatLineCnt - 1 : 0, // NotebookConcatTextDocument.lineCount
101+
this._lineCounts[1],
102+
];
103+
104+
this._textLen = [concatTextLen > 0 ? concatTextLen - 1 : 0, this._textLen[1]];
105+
}
106+
107+
private _updateInput() {
108+
this._lineCounts = [this._lineCounts[0], this._input?.lineCount ?? 0];
109+
110+
this._textLen = [this._textLen[0], this._getDocumentTextLen(this._input)];
111+
}
112+
113+
private _getDocumentTextLen(textDocument?: TextDocument): number {
114+
if (!textDocument) {
115+
return 0;
116+
}
117+
return textDocument.offsetAt(textDocument.lineAt(textDocument.lineCount - 1).range.end) + 1;
118+
}
119+
120+
getText(range?: Range): string {
121+
if (!range) {
122+
let result = '';
123+
result += `${this._concatTextDocument.getText()}\n${this._input?.getText() ?? ''}`;
124+
return result;
125+
}
126+
127+
if (range.isEmpty) {
128+
return '';
129+
}
130+
131+
const start = this.locationAt(range.start);
132+
const end = this.locationAt(range.end);
133+
134+
const startDocument = workspace.textDocuments.find(
135+
(document) => document.uri.toString() === start.uri.toString(),
136+
);
137+
const endDocument = workspace.textDocuments.find((document) => document.uri.toString() === end.uri.toString());
138+
139+
if (!startDocument || !endDocument) {
140+
return '';
141+
}
142+
if (startDocument === endDocument) {
143+
return startDocument.getText(start.range);
144+
}
145+
146+
const a = startDocument.getText(new Range(start.range.start, new Position(startDocument.lineCount, 0)));
147+
const b = endDocument.getText(new Range(new Position(0, 0), end.range.end));
148+
return `${a}\n${b}`;
149+
}
150+
151+
offsetAt(position: Position): number {
152+
const { line } = position;
153+
if (line >= this._lineCounts[0]) {
154+
// input box
155+
const lineOffset = Math.max(0, line - this._lineCounts[0] - 1);
156+
return this._input?.offsetAt(new Position(lineOffset, position.character)) ?? 0;
157+
}
158+
// concat
159+
return this._concatTextDocument.offsetAt(position);
160+
}
161+
162+
// turning an offset on the final concatenatd document to position
163+
positionAt(locationOrOffset: Location | number): Position {
164+
if (typeof locationOrOffset === 'number') {
165+
const concatTextLen = this._textLen[0];
166+
167+
if (locationOrOffset >= concatTextLen) {
168+
// in the input box
169+
const offset = Math.max(0, locationOrOffset - concatTextLen - 1);
170+
return this._input?.positionAt(offset) ?? new Position(0, 0);
171+
}
172+
const position = this._concatTextDocument.positionAt(locationOrOffset);
173+
return new Position(this._lineCounts[0] + position.line, position.character);
174+
}
175+
176+
if (locationOrOffset.uri.toString() === this._input?.uri.toString()) {
177+
// range in the input box
178+
return new Position(
179+
this._lineCounts[0] + locationOrOffset.range.start.line,
180+
locationOrOffset.range.start.character,
181+
);
182+
}
183+
return this._concatTextDocument.positionAt(locationOrOffset);
184+
}
185+
186+
locationAt(positionOrRange: Range | Position): Location {
187+
if (positionOrRange instanceof Position) {
188+
positionOrRange = new Range(positionOrRange, positionOrRange);
189+
}
190+
191+
const start = positionOrRange.start.line;
192+
if (start >= this._lineCounts[0]) {
193+
// this is the inputbox
194+
const offset = Math.max(0, start - this._lineCounts[0] - 1);
195+
const startPosition = new Position(offset, positionOrRange.start.character);
196+
const endOffset = Math.max(0, positionOrRange.end.line - this._lineCounts[0] - 1);
197+
const endPosition = new Position(endOffset, positionOrRange.end.character);
198+
199+
// TODO@rebornix !
200+
return new Location(this._input!.uri, new Range(startPosition, endPosition));
201+
}
202+
203+
// this is the NotebookConcatTextDocument
204+
return this._concatTextDocument.locationAt(positionOrRange);
205+
}
206+
207+
contains(uri: Uri): boolean {
208+
if (this._input?.uri.toString() === uri.toString()) {
209+
return true;
210+
}
211+
212+
return this._concatTextDocument.contains(uri);
213+
}
214+
215+
validateRange(range: Range): Range {
216+
return range;
217+
}
218+
219+
validatePosition(position: Position): Position {
220+
return position;
221+
}
222+
223+
lineAt(posOrNumber: Position | number): TextLine {
224+
const position = typeof posOrNumber === 'number' ? new Position(posOrNumber, 0) : posOrNumber;
225+
226+
// convert this position into a cell location
227+
// (we need the translated location, that's why we can't use getCellAtPosition)
228+
const location = this._concatTextDocument.locationAt(position);
229+
230+
// Get the cell at this location
231+
if (location.uri.toString() === this._input?.uri.toString()) {
232+
return this._input.lineAt(location.range.start);
233+
}
234+
235+
const cell = this._notebook.getCells().find((c) => c.document.uri.toString() === location.uri.toString());
236+
return cell!.document.lineAt(location.range.start);
237+
}
238+
239+
getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined {
240+
// convert this position into a cell location
241+
// (we need the translated location, that's why we can't use getCellAtPosition)
242+
const location = this._concatTextDocument.locationAt(position);
243+
244+
if (location.uri.toString() === this._input?.uri.toString()) {
245+
return this._input.getWordRangeAtPosition(location.range.start, regexp);
246+
}
247+
248+
// Get the cell at this location
249+
const cell = this._notebook.getCells().find((c) => c.document.uri.toString() === location.uri.toString());
250+
return cell!.document.getWordRangeAtPosition(location.range.start, regexp);
251+
}
252+
}

0 commit comments

Comments
 (0)