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

Commit e098cb7

Browse files
committed
Implement textDocument/rename
1 parent 168c60f commit e098cb7

File tree

3 files changed

+207
-3
lines changed

3 files changed

+207
-3
lines changed

src/test/typescript-service-helpers.ts

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import * as chai from 'chai';
22
import * as sinon from 'sinon';
33
import * as ts from 'typescript';
44
import { CompletionItemKind, CompletionList, TextDocumentIdentifier, TextDocumentItem } from 'vscode-languageserver';
5-
import { Hover, Location, SignatureHelp, SymbolInformation, SymbolKind } from 'vscode-languageserver-types';
5+
import { Hover, Location, SignatureHelp, SymbolInformation, SymbolKind, WorkspaceEdit } from 'vscode-languageserver-types';
66
import { LanguageClient, RemoteLanguageClient } from '../lang-handler';
77
import { TextDocumentContentParams, WorkspaceFilesParams } from '../request-type';
88
import { SymbolLocationInformation } from '../request-type';
99
import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service';
1010
import chaiAsPromised = require('chai-as-promised');
1111
import { apply } from 'json-patch';
12-
import { ITestCallbackContext } from 'mocha';
12+
import { ISuiteCallbackContext, ITestCallbackContext } from 'mocha';
1313
chai.use(chaiAsPromised);
1414
const assert = chai.assert;
1515

@@ -2172,6 +2172,144 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
21722172
});
21732173
} as any);
21742174

2175+
describe('textDocumentRename()', function (this: TestContext & ISuiteCallbackContext) {
2176+
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
2177+
[rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })],
2178+
[rootUri + 'a.ts', [
2179+
'class A {',
2180+
' /** foo doc*/',
2181+
' foo() {}',
2182+
' /** bar doc*/',
2183+
' bar(): number { return 1; }',
2184+
' /** baz doc*/',
2185+
' baz(): string { return ""; }',
2186+
' /** qux doc*/',
2187+
' qux: number;',
2188+
'}',
2189+
'const a = new A();',
2190+
'a.'
2191+
].join('\n')],
2192+
[rootUri + 'uses-import.ts', [
2193+
'import {d} from "./import"',
2194+
'const x = d();'
2195+
].join('\n')],
2196+
[rootUri + 'import.ts', 'export function d(): number { return 55; }']
2197+
])) as any);
2198+
2199+
afterEach(shutdownService as any);
2200+
2201+
it('should error on an invalid symbol', async function (this: TestContext & ITestCallbackContext) {
2202+
await assert.isRejected(
2203+
this.service.textDocumentRename({
2204+
textDocument: {
2205+
uri: rootUri + 'a.ts'
2206+
},
2207+
position: {
2208+
line: 0,
2209+
character: 1
2210+
},
2211+
newName: 'asdf'
2212+
}).toArray().map(patches => apply(null, patches)).toPromise(),
2213+
'This symbol cannot be renamed'
2214+
);
2215+
});
2216+
it('should return a correct WorkspaceEdit to rename a class', async function (this: TestContext & ITestCallbackContext) {
2217+
const result: WorkspaceEdit = await this.service.textDocumentRename({
2218+
textDocument: {
2219+
uri: rootUri + 'a.ts'
2220+
},
2221+
position: {
2222+
line: 0,
2223+
character: 6
2224+
},
2225+
newName: 'B'
2226+
}).toArray().map(patches => apply(null, patches)).toPromise();
2227+
assert.deepEqual(result, {
2228+
changes: {
2229+
[rootUri + 'a.ts']: [{
2230+
newText: 'B',
2231+
range: {
2232+
end: {
2233+
character: 7,
2234+
line: 0
2235+
},
2236+
start: {
2237+
character: 6,
2238+
line: 0
2239+
}
2240+
}
2241+
}, {
2242+
newText: 'B',
2243+
range: {
2244+
end: {
2245+
character: 15,
2246+
line: 10
2247+
},
2248+
start: {
2249+
character: 14,
2250+
line: 10
2251+
}
2252+
}
2253+
}]
2254+
}
2255+
});
2256+
});
2257+
it('should return a correct WorkspaceEdit to rename an imported function', async function (this: TestContext & ITestCallbackContext) {
2258+
const result: WorkspaceEdit = await this.service.textDocumentRename({
2259+
textDocument: {
2260+
uri: rootUri + 'import.ts'
2261+
},
2262+
position: {
2263+
line: 0,
2264+
character: 16
2265+
},
2266+
newName: 'f'
2267+
}).toArray().map(patches => apply(null, patches)).toPromise();
2268+
assert.deepEqual(result, {
2269+
changes: {
2270+
[rootUri + 'import.ts']: [{
2271+
newText: 'f',
2272+
range: {
2273+
end: {
2274+
character: 17,
2275+
line: 0
2276+
},
2277+
start: {
2278+
character: 16,
2279+
line: 0
2280+
}
2281+
}
2282+
}],
2283+
[rootUri + 'uses-import.ts']: [{
2284+
newText: 'f',
2285+
range: {
2286+
end: {
2287+
character: 9,
2288+
line: 0
2289+
},
2290+
start: {
2291+
character: 8,
2292+
line: 0
2293+
}
2294+
}
2295+
}, {
2296+
newText: 'f',
2297+
range: {
2298+
end: {
2299+
character: 11,
2300+
line: 1
2301+
},
2302+
start: {
2303+
character: 10,
2304+
line: 1
2305+
}
2306+
}
2307+
}]
2308+
}
2309+
});
2310+
});
2311+
});
2312+
21752313
describe('Special file names', function (this: TestContext) {
21762314

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

src/typescript-service.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import {
1919
MarkedString,
2020
ParameterInformation,
2121
ReferenceParams,
22+
RenameParams,
2223
SignatureHelp,
2324
SignatureInformation,
2425
SymbolInformation,
2526
TextDocumentPositionParams,
26-
TextDocumentSyncKind
27+
TextDocumentSyncKind,
28+
TextEdit,
29+
WorkspaceEdit
2730
} from 'vscode-languageserver';
2831
import { walkMostAST } from './ast';
2932
import { convertTsDiagnostic } from './diagnostics';
@@ -46,6 +49,7 @@ import {
4649
import {
4750
convertStringtoSymbolKind,
4851
defInfoToSymbolDescriptor,
52+
encodeJsonPointerComponent,
4953
isLocalUri,
5054
isSymbolDescriptorMatch,
5155
normalizeUri,
@@ -1004,6 +1008,61 @@ export class TypeScriptService {
10041008
.map(signatureHelp => ({ op: 'add', path: '', value: signatureHelp }) as AddPatch);
10051009
}
10061010

1011+
/**
1012+
* The rename request is sent from the client to the server to perform a workspace-wide rename of a symbol.
1013+
*
1014+
* @return Observable of JSON Patches that build a `WorkspaceEdit` result
1015+
*/
1016+
textDocumentRename(params: RenameParams, span = new Span()): Observable<OpPatch> {
1017+
const uri = normalizeUri(params.textDocument.uri);
1018+
const editUris = new Set<string>();
1019+
return Observable.fromPromise(this.projectManager.ensureOwnFiles(span))
1020+
.mergeMap(() => {
1021+
1022+
const filePath = uri2path(uri);
1023+
const configuration = this.projectManager.getParentConfiguration(params.textDocument.uri);
1024+
if (!configuration) {
1025+
throw new Error(`tsconfig.json not found for ${filePath}`);
1026+
}
1027+
configuration.ensureAllFiles(span);
1028+
1029+
const sourceFile = this._getSourceFile(configuration, filePath, span);
1030+
if (!sourceFile) {
1031+
throw new Error(`Expected source file ${filePath} to exist in configuration`);
1032+
}
1033+
1034+
const position = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character);
1035+
1036+
const renameInfo = configuration.getService().getRenameInfo(filePath, position);
1037+
if (!renameInfo.canRename) {
1038+
throw new Error('This symbol cannot be renamed');
1039+
}
1040+
1041+
return Observable.from(configuration.getService().findRenameLocations(filePath, position, false, true))
1042+
.map((location: ts.RenameLocation): [string, TextEdit] => {
1043+
const sourceFile = this._getSourceFile(configuration, location.fileName, span);
1044+
if (!sourceFile) {
1045+
throw new Error(`expected source file ${location.fileName} to exist in configuration`);
1046+
}
1047+
const editUri = path2uri(this.root, location.fileName);
1048+
const start = ts.getLineAndCharacterOfPosition(sourceFile, location.textSpan.start);
1049+
const end = ts.getLineAndCharacterOfPosition(sourceFile, location.textSpan.start + location.textSpan.length);
1050+
const edit: TextEdit = { range: { start, end }, newText: params.newName };
1051+
return [editUri, edit];
1052+
});
1053+
})
1054+
.map(([uri, edit]): AddPatch => {
1055+
// if file has no edit yet, initialize array
1056+
if (!editUris.has(uri)) {
1057+
editUris.add(uri);
1058+
return { op: 'add', path: `/changes/${encodeJsonPointerComponent(uri)}`, value: [edit] };
1059+
}
1060+
// else append to array
1061+
return { op: 'add', path: `/changes/${encodeJsonPointerComponent(uri)}/-`, value: edit };
1062+
})
1063+
.startWith({ op: 'add', path: '', value: { changes: {} } as WorkspaceEdit } as AddPatch);
1064+
}
1065+
10071066
/**
10081067
* The document open notification is sent from the client to the server to signal newly opened
10091068
* text documents. The document's truth is now managed by the client and the server must not try

src/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import { PackageDescriptor, SymbolDescriptor } from './request-type';
77

88
let strict = false;
99

10+
/**
11+
* Encodes a JSON Pointer Component as per https://tools.ietf.org/html/rfc6901#section-3
12+
*/
13+
export function encodeJsonPointerComponent(component: string): string {
14+
return component.replace(/~/g, '~0').replace(/\//g, '~1');
15+
}
16+
1017
/**
1118
* Toggles "strict" flag, affects how we are parsing/generating URLs.
1219
* In strict mode we using "file://PATH", otherwise on Windows we are using "file:///PATH"

0 commit comments

Comments
 (0)