Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 61 additions & 0 deletions src/test/typescript-service-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,67 @@ export function describeTypeScriptService(
})
})

describe('organizeImports()', () => {
beforeEach(
initializeTypeScriptService(
createService,
rootUri,
new Map([
[rootUri + 'package.json', JSON.stringify({ name: 'mypkg' })],
[
rootUri + 'a.ts',
[
'import {b, a} from "./import"',
'const x = a() + b();'
]
.join('\n'),
],
[
rootUri + 'import.ts',
[
'export function a(): number { return 11; }',
'export function b(): number { return 2; }'
]
.join('\n'),
]
])
)
)

afterEach(shutdownService)

it('should return a correct WorkspaceEdit to organize imports', async function(this: TestContext &
Context): Promise<void> {
const result: WorkspaceEdit = await this.service
.organizeImports({
textDocument: {
uri: rootUri + 'a.ts',
},
})
.reduce<Operation, WorkspaceEdit>(applyReducer, null as any)
.toPromise()
assert.deepEqual(result, {
changes: {
[rootUri + 'a.ts']: [
{
newText: 'import { a, b } from "./import";\n',
range: {
end: {
character: 0,
line: 1,
},
start: {
character: 0,
line: 0,
},
},
},
],
},
})
})
})

describe('textDocumentCodeAction()', () => {
beforeEach(
initializeTypeScriptService(
Expand Down
78 changes: 76 additions & 2 deletions src/typescript-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Observable } from 'rxjs'
import * as ts from 'typescript'
import * as url from 'url'
import {
CodeActionKind,
CodeActionParams,
Command,
CompletionItemKind,
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
ReferenceInformation,
SymbolDescriptor,
SymbolLocationInformation,
TextDocumentContentParams,
WorkspaceReferenceParams,
WorkspaceSymbolParams,
} from './request-type'
Expand Down Expand Up @@ -308,7 +310,9 @@ export class TypeScriptService {
resolveProvider: true,
triggerCharacters: ['.'],
},
codeActionProvider: true,
codeActionProvider: {
codeActionKinds: [ CodeActionKind.SourceOrganizeImports]
},
renameProvider: true,
executeCommandProvider: {
commands: [],
Expand Down Expand Up @@ -546,7 +550,7 @@ export class TypeScriptService {
.split('/')
.map(encodeURIComponent)
.join('/')
const parts: url.UrlObject = url.parse(uri)
const parts: url.UrlObject = url.parse(uri) as url.UrlObject
const packageJsonUri = url.format({
...parts,
pathname:
Expand Down Expand Up @@ -1578,6 +1582,66 @@ export class TypeScriptService {
.startWith({ op: 'add', path: '', value: { changes: {} } as WorkspaceEdit } as Operation)
}

/**
* Organize imports
*
* @return Observable of JSON Patches that build a `WorkspaceEdit` result
*/
public organizeImports(params: TextDocumentContentParams, span = new Span()): Observable<Operation> {
const uri = normalizeUri(params.textDocument.uri)
const editUris = new Set<string>()
return this.projectManager
.ensureOwnFiles(span)
.concat(
Observable.defer(() => {
const filePath = uri2path(uri)
const configuration = this.projectManager.getParentConfiguration(params.textDocument.uri)
if (!configuration) {
throw new Error(`tsconfig.json not found for ${filePath}`)
}
configuration.ensureAllFiles(span)

const sourceFile = this._getSourceFile(configuration, filePath, span)
if (!sourceFile) {
throw new Error(`Expected source file ${filePath} to exist in configuration`)
}

const scope: ts.OrganizeImportsScope = {
type: 'file',
fileName: filePath,
}
const formatOptions: ts.FormatCodeSettings = {}
formatOptions.insertSpaceAfterCommaDelimiter = true

const fileTextChanges: ts.FileTextChanges[] = configuration
.getService()
.organizeImports(scope, formatOptions, undefined) as ts.FileTextChanges[]

return Observable.from(fileTextChanges)
.mergeMap<ts.FileTextChanges, [string, TextEdit]>(fileChange => {
const sourceFile = this._getSourceFile(configuration, fileChange.fileName, span)
if (!sourceFile) {
throw new Error(`Expected source file ${fileChange.fileName} to exist in configuration`)
}
const editUri = path2uri(fileChange.fileName)
return fileChange.textChanges
.map((change): TextEdit => this._textChangeToTextEdit(sourceFile, change))
.map((edit): [string, TextEdit] => [editUri, edit])
})
})
)
.map(
([uri, edit]): Operation => {
if (!editUris.has(uri)) {
editUris.add(uri)
return { op: 'add', path: JSONPTR`/changes/${uri}`, value: [edit] }
}
return { op: 'add', path: JSONPTR`/changes/${uri}/-`, value: edit }
}
)
.startWith({ op: 'add', path: '', value: { changes: {} } as WorkspaceEdit } as Operation)
}

/**
* The initialized notification is sent from the client to the server after the client received
* the result of the initialize request but before the client is sending any other request or
Expand Down Expand Up @@ -1624,6 +1688,16 @@ export class TypeScriptService {
this._publishDiagnostics(uri)
}

private _textChangeToTextEdit(sourceFile: ts.SourceFileLike, change: ts.TextChange): TextEdit {
return {
range: {
start: ts.getLineAndCharacterOfPosition(sourceFile, change.span.start),
end: ts.getLineAndCharacterOfPosition(sourceFile, change.span.start + change.span.length),
},
newText: change.newText
}
}

/**
* Generates and publishes diagnostics for a given file
*
Expand Down