Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Introduce LSP mode for TSServer with some initial functionality #45347

Draft
wants to merge 40 commits into
base: feature/lspSession
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d1a8bc1
[WIP] Add stub for LSP version of IOSession
Feb 26, 2021
eb0da1e
[WIP] Implement message handling and responding
Feb 26, 2021
373a3f6
Merge branch 'main' into lspSession
Jun 9, 2021
ffe95d3
[WIP] copy message reading from vscode-jsonrpc
Jun 17, 2021
b5347c1
[WIP] allow onMessage to handle both protocols
Jun 23, 2021
dc05b4e
[WIP] split out handler references
Jun 23, 2021
e8f7ff3
[WIP] add handling for initialized notification
Jun 25, 2021
44f30b7
[WIP] attempt to clean up response serialization
Jun 25, 2021
96e1daa
[WIP] port messagewriter
Jun 28, 2021
ff280d0
Enable lsp responses
Jun 28, 2021
a804110
Add shutdown and exit
Jun 28, 2021
bb31ff7
Receive text sync events
Jun 28, 2021
9201fcb
[WIP] add uri parsing
Jun 28, 2021
4ef8a61
Implement didOpen
Jun 28, 2021
2269c14
Implement didChange
Jun 28, 2021
b627420
Start to implement hover
Jun 29, 2021
7473d2d
Add markdown support
Jun 29, 2021
3d06a43
Implement signature help
Jun 29, 2021
fd51987
Add hover documentation
Jun 29, 2021
cb31d9c
Refactor lsp handlers to their own file
Jun 29, 2021
4ec5089
Address build issues in tests
Jun 29, 2021
b3d583c
Refactor session inheritance
Jun 29, 2021
01855a7
Extract lsp session logic for reuse in tests
Jun 29, 2021
033d196
start writing test infra
Jul 13, 2021
b4a419a
Merge remote-tracking branch 'upstream/main' into lspSession
Jul 13, 2021
f2c1828
Fix build errors and update test
Jul 13, 2021
4e8758f
Baseline instead of deepEqual
Jul 13, 2021
4fd72e8
Start fixing lint errors
Jul 15, 2021
205932b
Merge remote-tracking branch 'upstream/main' into lspSession
Jul 15, 2021
7fd3840
Finish fixing lint issues
Jul 15, 2021
22863c3
Merge branch 'microsoft:main' into lspSession
uniqueiniquity Jul 16, 2021
edbcc6d
add test for hover documentation and tags
Jul 20, 2021
980bbfd
Add basic signature help test
Jul 20, 2021
7db22b9
Add test for signature help documentation
Jul 20, 2021
c4a85fc
[WIP] start writing test for sh context
Jul 20, 2021
615babf
Merge branch 'lspSession' of https://github.com/uniqueiniquity/TypeSc…
Jul 20, 2021
dc1df99
Finish initial lsp sig help tests
Jul 20, 2021
23d335b
Execute protocol handlers in Node check phase
Jul 22, 2021
2d3d650
Introduce map for message queue for cancellation
Jul 23, 2021
16ac035
Merge branch 'main' into lspSession
Aug 6, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"typescript": "^4.2.3",
"vinyl": "latest",
"vinyl-sourcemaps-apply": "latest",
"vscode-uri": "^3.0.2",
"xml2js": "^0.4.19"
},
"scripts": {
Expand Down
17 changes: 17 additions & 0 deletions src/jsonrpc/disposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* @internal */
namespace ts.server.rpc {
export interface Disposable {
/**
* Dispose this object.
*/
dispose(): void;
}

export namespace Disposable {
export function create(func: () => void): Disposable {
return {
dispose: func
};
}
}
}
127 changes: 127 additions & 0 deletions src/jsonrpc/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*@internal*/
namespace ts.server.rpc {
export interface FunctionContentEncoder {
name: string;
encode(input: Uint8Array): Promise<Uint8Array>;
}

export interface StreamContentEncoder {
name: string;
create(): RAL.WritableStream;
}

export type ContentEncoder = FunctionContentEncoder | (FunctionContentEncoder & StreamContentEncoder);

export interface FunctionContentDecoder {
name: string;
decode(buffer: Uint8Array): Promise<Uint8Array>;
}

export interface StreamContentDecoder {
name: string;
create(): RAL.WritableStream;
}

export type ContentDecoder = FunctionContentDecoder | (FunctionContentDecoder & StreamContentDecoder);

export interface ContentTypeEncoderOptions {
charset: RAL.MessageBufferEncoding;
}

export interface FunctionContentTypeEncoder {
name: string;
encode(msg: Message, options: ContentTypeEncoderOptions): Promise<Uint8Array>;
}

export interface StreamContentTypeEncoder {
name: string;
create(options: ContentTypeEncoderOptions): RAL.WritableStream;
}

export type ContentTypeEncoder = FunctionContentTypeEncoder | (FunctionContentTypeEncoder & StreamContentTypeEncoder);

export interface ContentTypeDecoderOptions {
charset: RAL.MessageBufferEncoding;
}

export interface FunctionContentTypeDecoder {
name: string;
decode(buffer: Uint8Array, options: ContentTypeDecoderOptions): Promise<Message>
}

export interface StreamContentTypeDecoder {
name: string;
create(options: ContentTypeDecoderOptions): RAL.WritableStream;
}

export type ContentTypeDecoder = FunctionContentTypeDecoder | (FunctionContentTypeDecoder & StreamContentTypeDecoder);

interface Named {
name: string;
}

export namespace Encodings {

export function getEncodingHeaderValue(encodings: Named[]): string | undefined {
if (encodings.length === 1) {
return encodings[0].name;
}
const distribute = encodings.length - 1;
if (distribute > 1000) {
throw new Error(`Quality value can only have three decimal digits but trying to distribute ${encodings.length} elements.`);
}
const digits = Math.ceil(Math.log10(distribute));
const factor = Math.pow(10,digits);
const diff = Math.floor((1 / distribute) * factor) / factor;

const result: string[] = [];
let q = 1;
for (const encoding of encodings) {
result.push(`${encoding.name};q=${q === 1 || q === 0 ? q.toFixed(0) : q.toFixed(digits)}`);
q = q - diff;
}
return result.join(", ");
}

export function parseEncodingHeaderValue(value: string): string[] {
const map: Map<number, string[]> = new Map();
const encodings = value.split(/\s*,\s*/);
for (const value of encodings) {
const [encoding, q] = parseEncoding(value);
if (encoding === "*") {
continue;
}
let values = map.get(q);
if (values === undefined) {
values = [];
map.set(q, values);
}
values.push(encoding);
}
const keys = Array.from(map.keys());
keys.sort((a, b) => b - a);
const result: string[] = [];
for (const key of keys) {
result.push(...map.get(key)!);
}
return result;
}

function parseEncoding(value: string): [string, number] {
let q = 1;
let encoding: string;
const index = value.indexOf(";q=");
if (index !== -1) {
const parsed = parseFloat(value.substr(index));
if (!isNaN(parsed)) {
q = parsed;
}
encoding = value.substr(0, index);
}
else {
encoding = value;
}
return [encoding, q];
}
}
}
192 changes: 192 additions & 0 deletions src/jsonrpc/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/* eslint-disable only-arrow-functions */
/* eslint-disable @typescript-eslint/prefer-function-type */
/* @internal */
namespace ts.server.rpc {
/**
* Represents a typed event.
*/
export interface Event<T> {
/**
*
* @param listener The listener function will be call when the event happens.
* @param thisArgs The 'this' which will be used when calling the event listener.
* @param disposables An array to which a {{IDisposable}} will be added. The
* @return
*/
(
listener: (e: T) => any,
thisArgs?: any,
disposables?: Disposable[]
): Disposable;
}

export namespace Event {
const _disposable = { dispose() {} };
export const None: Event<any> = function () {
return _disposable;
};
}

class CallbackList {
private _callbacks: Function[] | undefined;
private _contexts: any[] | undefined;

public add(
callback: Function,
// eslint-disable-next-line no-null/no-null
context: any = null,
bucket?: Disposable[]
): void {
if (!this._callbacks) {
this._callbacks = [];
this._contexts = [];
}
this._callbacks.push(callback);
this._contexts!.push(context);

if (Array.isArray(bucket)) {
bucket.push({ dispose: () => this.remove(callback, context) });
}
}

// eslint-disable-next-line no-null/no-null
public remove(callback: Function, context: any = null): void {
if (!this._callbacks) {
return;
}

let foundCallbackWithDifferentContext = false;
for (let i = 0, len = this._callbacks.length; i < len; i++) {
if (this._callbacks[i] === callback) {
if (this._contexts![i] === context) {
// callback & context match => remove it
this._callbacks.splice(i, 1);
this._contexts!.splice(i, 1);
return;
}
else {
foundCallbackWithDifferentContext = true;
}
}
}

if (foundCallbackWithDifferentContext) {
throw new Error(
"When adding a listener with a context, you should remove it with the same context"
);
}
}

public invoke(...args: any[]): any[] {
if (!this._callbacks) {
return [];
}

const ret: any[] = [],
callbacks = this._callbacks.slice(0),
contexts = this._contexts!.slice(0);

for (let i = 0, len = callbacks.length; i < len; i++) {
try {
ret.push(callbacks[i].apply(contexts[i], args));
}
catch (e) {
// eslint-disable-next-line no-console
RAL().console.error(e);
}
}
return ret;
}

public isEmpty(): boolean {
return !this._callbacks || this._callbacks.length === 0;
}

public dispose(): void {
this._callbacks = undefined;
this._contexts = undefined;
}
}

export interface EmitterOptions {
onFirstListenerAdd?: Function;
onLastListenerRemove?: Function;
}

export class Emitter<T> {
private static _noop = function () {};

private _event: Event<T> | undefined;
private _callbacks: CallbackList | undefined;

constructor(private _options?: EmitterOptions) {}

/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event(): Event<T> {
if (!this._event) {
this._event = (
listener: (e: T) => any,
thisArgs?: any,
disposables?: Disposable[]
) => {
if (!this._callbacks) {
this._callbacks = new CallbackList();
}
if (
this._options &&
this._options.onFirstListenerAdd &&
this._callbacks.isEmpty()
) {
this._options.onFirstListenerAdd(this);
}
this._callbacks.add(listener, thisArgs);

const result: Disposable = {
dispose: () => {
if (!this._callbacks) {
// disposable is disposed after emitter is disposed.
return;
}

this._callbacks.remove(listener, thisArgs);
result.dispose = Emitter._noop;
if (
this._options &&
this._options.onLastListenerRemove &&
this._callbacks.isEmpty()
) {
this._options.onLastListenerRemove(this);
}
},
};
if (Array.isArray(disposables)) {
disposables.push(result);
}

return result;
};
}
return this._event;
}

/**
* To be kept private to fire an event to
* subscribers
*/
fire(event: T): any {
if (this._callbacks) {
this._callbacks.invoke.call(this._callbacks, event);
}
}

dispose() {
if (this._callbacks) {
this._callbacks.dispose();
this._callbacks = undefined;
}
}
}
}
30 changes: 30 additions & 0 deletions src/jsonrpc/is.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*@internal*/
namespace ts.server.rpc.is {
export function boolean(value: any): value is boolean {
return value === true || value === false;
}

export function string(value: any): value is string {
return typeof value === "string" || value instanceof String;
}

export function number(value: any): value is number {
return typeof value === "number" || value instanceof Number;
}

export function error(value: any): value is Error {
return value instanceof Error;
}

export function func(value: any): value is Function {
return typeof value === "function";
}

export function array<T>(value: any): value is T[] {
return Array.isArray(value);
}

export function stringArray(value: any): value is string[] {
return array(value) && (value as any[]).every((elem) => string(elem));
}
}
14 changes: 14 additions & 0 deletions src/jsonrpc/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*@internal*/
namespace ts.server.rpc {
export class StreamMessageReader extends ReadableStreamMessageReader {
public constructor(readble: NodeJS.ReadableStream, encoding?: RAL.MessageBufferEncoding | MessageReaderOptions) {
super(RIL().stream.asReadableStream(readble), encoding);
}
}

export class StreamMessageWriter extends WriteableStreamMessageWriter {
public constructor(writable: NodeJS.WritableStream, options?: RAL.MessageBufferEncoding | MessageWriterOptions) {
super(RIL().stream.asWritableStream(writable), options);
}
}
}
Loading