Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.

Commit 8e7d2fa

Browse files
authored
Implement textDocument/codeAction (#252)
1 parent 6436c74 commit 8e7d2fa

File tree

4 files changed

+297
-4
lines changed

4 files changed

+297
-4
lines changed

src/lang-handler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { FORMAT_TEXT_MAP, Span } from 'opentracing';
33
import { inspect } from 'util';
44
import { isReponseMessage, Message, NotificationMessage, RequestMessage, ResponseMessage } from 'vscode-jsonrpc/lib/messages';
55
import {
6+
ApplyWorkspaceEditParams,
7+
ApplyWorkspaceEditResponse,
68
LogMessageParams,
79
PublishDiagnosticsParams,
810
TextDocumentIdentifier,
@@ -56,6 +58,13 @@ export interface LanguageClient {
5658
* @param params The diagnostics to send to the client
5759
*/
5860
textDocumentPublishDiagnostics(params: PublishDiagnosticsParams): void;
61+
62+
/**
63+
* Requests a set of text changes to be applied to documents in the workspace
64+
* Can occur as as a result of rename or executeCommand (code action).
65+
* @param params The edits to apply to the workspace
66+
*/
67+
workspaceApplyEdit(params: ApplyWorkspaceEditParams, childOf?: Span): Promise<ApplyWorkspaceEditResponse>;
5968
}
6069

6170
/**
@@ -186,4 +195,14 @@ export class RemoteLanguageClient {
186195
textDocumentPublishDiagnostics(params: PublishDiagnosticsParams): void {
187196
this.notify('textDocument/publishDiagnostics', params);
188197
}
198+
199+
/**
200+
* The workspace/applyEdit request is sent from the server to the client to modify resource on
201+
* the client side.
202+
*
203+
* @param params The edits to apply.
204+
*/
205+
workspaceApplyEdit(params: ApplyWorkspaceEditParams, childOf = new Span()): Promise<ApplyWorkspaceEditResponse> {
206+
return this.request('workspace/applyEdit', params, childOf).toPromise();
207+
}
189208
}

src/project-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,12 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost {
622622
return '' + this.projectVersion;
623623
}
624624

625+
getNewLine(): string {
626+
// Although this is optional, language service was sending edits with carriage returns if not specified.
627+
// TODO: combine with the FormatOptions defaults.
628+
return '\n';
629+
}
630+
625631
/**
626632
* Incrementing current project version, telling TS compiler to invalidate internal data
627633
*/

src/test/typescript-service-helpers.ts

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as chai from 'chai';
22
import * as sinon from 'sinon';
33
import * as ts from 'typescript';
4-
import { CompletionItemKind, CompletionList, TextDocumentIdentifier, TextDocumentItem } from 'vscode-languageserver';
5-
import { Hover, Location, SignatureHelp, SymbolInformation, SymbolKind, WorkspaceEdit } from 'vscode-languageserver-types';
4+
import { CompletionItemKind, CompletionList, DiagnosticSeverity, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver';
5+
import { Command, Diagnostic, Hover, Location, SignatureHelp, SymbolInformation, SymbolKind } from 'vscode-languageserver-types';
66
import { LanguageClient, RemoteLanguageClient } from '../lang-handler';
77
import { TextDocumentContentParams, WorkspaceFilesParams } from '../request-type';
88
import { SymbolLocationInformation } from '../request-type';
@@ -16,7 +16,7 @@ const assert = chai.assert;
1616
/**
1717
* Enforcing strict mode to make tests pass on Windows
1818
*/
19-
import { setStrict } from '../util';
19+
import { setStrict, uri2path } from '../util';
2020
setStrict(true);
2121

2222
export interface TestContext {
@@ -53,6 +53,7 @@ export const initializeTypeScriptService = (createService: TypeScriptServiceFact
5353
return Array.from(files.keys()).map(uri => ({ uri }));
5454
});
5555
this.client.xcacheGet.returns(null);
56+
this.client.workspaceApplyEdit.returns(Promise.resolve({applied: true}));
5657
this.service = createService(this.client);
5758

5859
await this.service.initialize({
@@ -2310,6 +2311,126 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
23102311
});
23112312
});
23122313

2314+
describe('textDocumentCodeAction()', function (this: TestContext & ISuiteCallbackContext) {
2315+
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
2316+
[rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })],
2317+
[rootUri + 'a.ts', [
2318+
'class A {',
2319+
'\tconstructor() {',
2320+
'\t\tmissingThis = 33;',
2321+
'\t}',
2322+
'}',
2323+
'const a = new A();'
2324+
].join('\n')]
2325+
])) as any);
2326+
2327+
afterEach(shutdownService as any);
2328+
2329+
it('suggests a missing this', async function (this: TestContext & ITestCallbackContext) {
2330+
await this.service.textDocumentDidOpen({
2331+
textDocument: {
2332+
uri: rootUri + 'a.ts',
2333+
languageId: 'typescript',
2334+
text: [
2335+
'class A {',
2336+
'\tmissingThis: number;',
2337+
'\tconstructor() {',
2338+
'\t\tmissingThis = 33;',
2339+
'\t}',
2340+
'}',
2341+
'const a = new A();'
2342+
].join('\n'),
2343+
version: 1
2344+
}
2345+
});
2346+
2347+
const firstDiagnostic: Diagnostic = {
2348+
range: {
2349+
start: { line: 3, character: 4 },
2350+
end: { line: 3, character: 15 }
2351+
},
2352+
message: 'Cannot find name \'missingThis\'. Did you mean the instance member \'this.missingThis\'?',
2353+
severity: DiagnosticSeverity.Error,
2354+
code: 2663,
2355+
source: 'ts'
2356+
};
2357+
const actions: Command[] = await this.service.textDocumentCodeAction({
2358+
textDocument: {
2359+
uri: rootUri + 'a.ts'
2360+
},
2361+
range: firstDiagnostic.range,
2362+
context: {
2363+
diagnostics: [firstDiagnostic]
2364+
}
2365+
}).toArray().map(patches => apply(null, patches)).toPromise();
2366+
assert.deepEqual(actions, [{
2367+
title: 'Add \'this.\' to unresolved variable.',
2368+
command: 'codeFix',
2369+
arguments: [{
2370+
fileName: uri2path(rootUri + 'a.ts'),
2371+
textChanges: [{
2372+
span: { start: 49, length: 13 },
2373+
newText: '\t\tthis.missingThis'
2374+
}]
2375+
}]
2376+
}]);
2377+
2378+
});
2379+
});
2380+
2381+
describe('workspaceExecuteCommand()', function (this: TestContext & ISuiteCallbackContext) {
2382+
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
2383+
[rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })],
2384+
[rootUri + 'a.ts', [
2385+
'class A {',
2386+
' constructor() {',
2387+
' missingThis = 33;',
2388+
' }',
2389+
'}',
2390+
'const a = new A();'
2391+
].join('\n')]
2392+
])) as any);
2393+
2394+
afterEach(shutdownService as any);
2395+
2396+
describe('codeFix', () => {
2397+
it('should apply a WorkspaceEdit for the passed FileTextChanges', async function (this: TestContext & ITestCallbackContext) {
2398+
await this.service.workspaceExecuteCommand({
2399+
command: 'codeFix',
2400+
arguments: [{
2401+
fileName: uri2path(rootUri + 'a.ts'),
2402+
textChanges: [{
2403+
span: { start: 50, length: 15 },
2404+
newText: '\t\tthis.missingThis'
2405+
}]
2406+
}]
2407+
}).toArray().map(patches => apply(null, patches)).toPromise();
2408+
2409+
sinon.assert.calledOnce(this.client.workspaceApplyEdit);
2410+
const workspaceEdit = this.client.workspaceApplyEdit.lastCall.args[0];
2411+
assert.deepEqual(workspaceEdit, {
2412+
edit: {
2413+
changes: {
2414+
[rootUri + 'a.ts']: [{
2415+
newText: '\t\tthis.missingThis',
2416+
range: {
2417+
end: {
2418+
character: 9,
2419+
line: 5
2420+
},
2421+
start: {
2422+
character: 0,
2423+
line: 3
2424+
}
2425+
}
2426+
}]
2427+
}
2428+
}
2429+
});
2430+
});
2431+
});
2432+
});
2433+
23132434
describe('Special file names', function (this: TestContext) {
23142435

23152436
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([

0 commit comments

Comments
 (0)