diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index ce610537ec499..795a1ccfd4ca1 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -70,12 +70,13 @@ namespace ts { // Literals + /* @internal */ export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote: boolean): StringLiteral; // tslint:disable-line unified-signatures /** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; export function createLiteral(value: number): NumericLiteral; export function createLiteral(value: boolean): BooleanLiteral; export function createLiteral(value: string | number | boolean): PrimaryExpression; - export function createLiteral(value: string | number | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): PrimaryExpression { + export function createLiteral(value: string | number | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote?: boolean): PrimaryExpression { if (typeof value === "number") { return createNumericLiteral(value + ""); } @@ -83,7 +84,9 @@ namespace ts { return value ? createTrue() : createFalse(); } if (isString(value)) { - return createStringLiteral(value); + const res = createStringLiteral(value); + if (isSingleQuote) res.singleQuote = true; + return res; } return createLiteralFromNode(value); } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 4da623746e8c3..4c0b775a6175a 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -365,7 +365,6 @@ namespace FourSlash { function memoWrap(ls: ts.LanguageService, target: TestState): ts.LanguageService { const cacheableMembers: (keyof typeof ls)[] = [ - "getCompletionsAtPosition", "getCompletionEntryDetails", "getCompletionEntrySymbol", "getQuickInfoAtPosition", @@ -1228,8 +1227,8 @@ Actual: ${stringify(fullActual)}`); return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options); } - private getCompletionEntryDetails(entryName: string, source?: string): ts.CompletionEntryDetails { - return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source); + private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails { + return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences); } private getReferencesAtCaret() { @@ -1728,8 +1727,8 @@ Actual: ${stringify(fullActual)}`); Harness.IO.log(stringify(sigHelp)); } - public printCompletionListMembers(options: ts.GetCompletionsAtPositionOptions | undefined) { - const completions = this.getCompletionListAtCaret(options); + public printCompletionListMembers(preferences: ts.UserPreferences | undefined) { + const completions = this.getCompletionListAtCaret(preferences); this.printMembersOrCompletions(completions); } @@ -1827,7 +1826,7 @@ Actual: ${stringify(fullActual)}`); } else if (prevChar === " " && /A-Za-z_/.test(ch)) { /* Completions */ - this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, ts.defaultPreferences); } if (i % checkCadence === 0) { @@ -2402,14 +2401,14 @@ Actual: ${stringify(fullActual)}`); public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) { this.goToMarker(markerName); - const actualCompletion = this.getCompletionListAtCaret({ includeExternalModuleExports: true, includeInsertTextCompletions: false }).entries.find(e => + const actualCompletion = this.getCompletionListAtCaret({ ...ts.defaultPreferences, includeCompletionsForModuleExports: true }).entries.find(e => e.name === options.name && e.source === options.source); if (!actualCompletion.hasAction) { this.raiseError(`Completion for ${options.name} does not have an associated action.`); } - const details = this.getCompletionEntryDetails(options.name, actualCompletion.source); + const details = this.getCompletionEntryDetails(options.name, actualCompletion.source, options.preferences); if (details.codeActions.length !== 1) { this.raiseError(`Expected one code action, got ${details.codeActions.length}`); } @@ -2454,7 +2453,7 @@ Actual: ${stringify(fullActual)}`); const { fixId, newFileContent } = options; const fixIds = ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => a.fixId); ts.Debug.assert(ts.contains(fixIds, fixId), "No available code fix has that group id.", () => `Expected '${fixId}'. Available action ids: ${fixIds}`); - const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings); + const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.defaultPreferences); assert.deepEqual(commands, options.commands); assert(changes.every(c => c.fileName === this.activeFile.fileName), "TODO: support testing codefixes that touch multiple files"); this.applyChanges(changes); @@ -2483,7 +2482,7 @@ Actual: ${stringify(fullActual)}`); public verifyCodeFix(options: FourSlashInterface.VerifyCodeFixOptions) { const fileName = this.activeFile.fileName; - const actions = this.getCodeFixes(fileName, options.errorCode); + const actions = this.getCodeFixes(fileName, options.errorCode, options.preferences); let index = options.index; if (index === undefined) { if (!(actions && actions.length === 1)) { @@ -2522,7 +2521,7 @@ Actual: ${stringify(fullActual)}`); * Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found. * @param fileName Path to file where error should be retrieved from. */ - private getCodeFixes(fileName: string, errorCode?: number): ts.CodeFixAction[] { + private getCodeFixes(fileName: string, errorCode?: number, preferences: ts.UserPreferences = ts.defaultPreferences): ts.CodeFixAction[] { const diagnosticsForCodeFix = this.getDiagnostics(fileName, /*includeSuggestions*/ true).map(diagnostic => ({ start: diagnostic.start, length: diagnostic.length, @@ -2534,7 +2533,7 @@ Actual: ${stringify(fullActual)}`); return; } - return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.start + diagnostic.length, [diagnostic.code], this.formatCodeSettings); + return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.start + diagnostic.length, [diagnostic.code], this.formatCodeSettings, preferences); }); } @@ -2560,7 +2559,7 @@ Actual: ${stringify(fullActual)}`); } } - public verifyImportFixAtPosition(expectedTextArray: string[], errorCode?: number) { + public verifyImportFixAtPosition(expectedTextArray: string[], errorCode: number | undefined, preferences: ts.UserPreferences | undefined) { const { fileName } = this.activeFile; const ranges = this.getRanges().filter(r => r.fileName === fileName); if (ranges.length !== 1) { @@ -2568,7 +2567,7 @@ Actual: ${stringify(fullActual)}`); } const range = ts.first(ranges); - const codeFixes = this.getCodeFixes(fileName, errorCode); + const codeFixes = this.getCodeFixes(fileName, errorCode, preferences); if (codeFixes.length === 0) { if (expectedTextArray.length !== 0) { @@ -2938,7 +2937,7 @@ Actual: ${stringify(fullActual)}`); public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) { const marker = this.getMarkerByName(markerName); - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position); + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position, ts.defaultPreferences); const isAvailable = applicableRefactors && applicableRefactors.length > 0; if (negative && isAvailable) { this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`); @@ -2958,7 +2957,7 @@ Actual: ${stringify(fullActual)}`); public verifyRefactorAvailable(negative: boolean, name: string, actionName?: string) { const selection = this.getSelection(); - let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || []; + let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || []; refactors = refactors.filter(r => r.name === name && (actionName === undefined || r.actions.some(a => a.name === actionName))); const isAvailable = refactors.length > 0; @@ -2980,7 +2979,7 @@ Actual: ${stringify(fullActual)}`); public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) { const selection = this.getSelection(); - const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || ts.emptyArray) + const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection, ts.defaultPreferences) || ts.emptyArray) .filter(r => r.name === name && r.actions.some(a => a.name === actionName)); this.assertObjectsEqual(actualRefactors, refactors); } @@ -2991,7 +2990,7 @@ Actual: ${stringify(fullActual)}`); throw new Error("Exactly one refactor range is allowed per test."); } - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, { pos: ranges[0].pos, end: ranges[0].end }); + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, ts.first(ranges), ts.defaultPreferences); const isAvailable = applicableRefactors && applicableRefactors.length > 0; if (negative && isAvailable) { this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`); @@ -3003,7 +3002,7 @@ Actual: ${stringify(fullActual)}`); public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) { const range = this.getSelection(); - const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range); + const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range, ts.defaultPreferences); const refactorsWithName = refactors.filter(r => r.name === refactorName); if (refactorsWithName.length === 0) { this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`); @@ -3017,7 +3016,7 @@ Actual: ${stringify(fullActual)}`); this.raiseError(`Expected action description to be ${JSON.stringify(actionDescription)}, got: ${JSON.stringify(action.description)}`); } - const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName); + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName, ts.defaultPreferences); for (const edit of editInfo.edits) { this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); } @@ -3062,14 +3061,14 @@ Actual: ${stringify(fullActual)}`); formattingOptions = formattingOptions || this.formatCodeSettings; const markerPos = this.getMarkerByName(markerName).position; - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos); + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos, ts.defaultPreferences); const applicableRefactorToApply = ts.find(applicableRefactors, refactor => refactor.name === refactorNameToApply); if (!applicableRefactorToApply) { this.raiseError(`The expected refactor: ${refactorNameToApply} is not available at the marker location.`); } - const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply, actionName); + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply, actionName, ts.defaultPreferences); for (const edit of editInfo.edits) { this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); @@ -4217,8 +4216,8 @@ namespace FourSlashInterface { this.state.applyCodeActionFromCompletion(markerName, options); } - public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { - this.state.verifyImportFixAtPosition(expectedTextArray, errorCode); + public importFixAtPosition(expectedTextArray: string[], errorCode?: number, preferences?: ts.UserPreferences): void { + this.state.verifyImportFixAtPosition(expectedTextArray, errorCode, preferences); } public navigationBar(json: any, options?: { checkSpans?: boolean }) { @@ -4424,7 +4423,7 @@ namespace FourSlashInterface { this.state.printCurrentSignatureHelp(); } - public printCompletionListMembers(options: ts.GetCompletionsAtPositionOptions | undefined) { + public printCompletionListMembers(options: ts.UserPreferences | undefined) { this.state.printCompletionListMembers(options); } @@ -4621,11 +4620,11 @@ namespace FourSlashInterface { } export type ExpectedCompletionEntry = string | { name: string, insertText?: string, replacementSpan?: FourSlash.Range }; - export interface CompletionsAtOptions extends ts.GetCompletionsAtPositionOptions { + export interface CompletionsAtOptions extends Partial { isNewIdentifierLocation?: boolean; } - export interface VerifyCompletionListContainsOptions extends ts.GetCompletionsAtPositionOptions { + export interface VerifyCompletionListContainsOptions extends ts.UserPreferences { sourceDisplay: string; isRecommended?: true; insertText?: string; @@ -4646,6 +4645,7 @@ namespace FourSlashInterface { description: string; errorCode?: number; index?: number; + preferences?: ts.UserPreferences; } export interface VerifyCodeFixAvailableOptions { @@ -4669,6 +4669,7 @@ namespace FourSlashInterface { name: string; source?: string; description: string; + preferences?: ts.UserPreferences; } export interface Diagnostic { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 23aa5ee2b8d58..17788f6251d71 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -420,11 +420,11 @@ namespace Harness.LanguageService { getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); } - getCompletionsAtPosition(fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined): ts.CompletionInfo { - return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, options)); + getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { + return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: ts.FormatCodeOptions | undefined, source: string | undefined): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(options), source)); + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); } getCompletionEntrySymbol(): ts.Symbol { throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 1e3e3c8c81a0d..eaf90a211ccdc 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -127,6 +127,7 @@ namespace ts { endPosition: selectionRange.end, host: notImplementedHost, formatContext: formatting.getFormatContext(testFormatOptions), + preferences: defaultPreferences, }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); @@ -190,6 +191,7 @@ namespace ts { endPosition: selectionRange.end, host: notImplementedHost, formatContext: formatting.getFormatContext(testFormatOptions), + preferences: defaultPreferences, }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); diff --git a/src/harness/unittests/organizeImports.ts b/src/harness/unittests/organizeImports.ts index bb5c9cbee1867..de4a4722d67dc 100644 --- a/src/harness/unittests/organizeImports.ts +++ b/src/harness/unittests/organizeImports.ts @@ -193,7 +193,7 @@ export const Other = 1; content: "function F() { }", }; const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatOptions); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatOptions, defaultPreferences); assert.isEmpty(changes); }); @@ -403,7 +403,7 @@ import { React, Other } from "react"; function runBaseline(baselinePath: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) { const { path: testPath, content: testContent } = testFile; const languageService = makeLanguageService(testFile, ...otherFiles); - const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions); + const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions, defaultPreferences); assert.equal(changes.length, 1); assert.equal(changes[0].fileName, testPath); diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 9f4a2683ee2d6..a892d28808cf8 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -145,7 +145,7 @@ namespace ts.server { session.onMessage(JSON.stringify(configureRequest)); - assert.equal(session.getProjectService().getFormatCodeOptions().indentStyle, IndentStyle.Block); + assert.equal(session.getProjectService().getFormatCodeOptions("" as NormalizedPath).indentStyle, IndentStyle.Block); const setOptionsRequest: protocol.SetCompilerOptionsForInferredProjectsRequest = { command: CommandNames.CompilerOptionsForInferredProjects, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 68defb6c4a755..0a619da1cd39a 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1327,13 +1327,13 @@ namespace ts.projectSystem { service.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, defaultPreferences); // should contain completions for string assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, defaultPreferences); // should contain completions for string assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); @@ -1359,11 +1359,11 @@ namespace ts.projectSystem { service.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, defaultPreferences); assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, defaultPreferences); assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); const sf2 = service.externalProjects[0].getLanguageService().getProgram().getSourceFile(f2.path); assert.equal(sf2.text, ""); @@ -1968,7 +1968,7 @@ namespace ts.projectSystem { // Check identifiers defined in HTML content are available in .ts file const project = configuredProjectAt(projectService, 0); - let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, defaultPreferences); assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); // Close HTML file @@ -1982,7 +1982,7 @@ namespace ts.projectSystem { checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Check identifiers defined in HTML content are not available in .ts file - completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, defaultPreferences); assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); }); @@ -2635,7 +2635,7 @@ namespace ts.projectSystem { assert.equal(lastEvent.data.project, project, "project name"); assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state"); - const options = projectService.getFormatCodeOptions(); + const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); }); @@ -3580,7 +3580,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(f1.path); - const defaultSettings = projectService.getFormatCodeOptions(); + const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); // set global settings const newGlobalSettings1 = clone(defaultSettings); diff --git a/src/server/client.ts b/src/server/client.ts index 578cbe507bc00..1a0912d378cc2 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -169,8 +169,9 @@ namespace ts.server { }; } - getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo { - const args: protocol.CompletionsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), ...options }; + getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { + // Not passing along 'preferences' because server should already have those from the 'configure' command + const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); const request = this.processRequest(CommandNames.Completions, args); const response = this.processResponse(request); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c46e2fa68752a..ba0a80890fe8e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -200,6 +200,7 @@ namespace ts.server { export interface HostConfiguration { formatCodeOptions: FormatCodeSettings; + preferences: UserPreferences; hostInfo: string; extraFileExtensions?: JsFileExtensionInfo[]; } @@ -438,6 +439,7 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), + preferences: defaultPreferences, hostInfo: "Unknown host", extraFileExtensions: [] }; @@ -683,15 +685,14 @@ namespace ts.server { return project.dirty && project.updateGraph(); } - getFormatCodeOptions(file?: NormalizedPath) { - let formatCodeSettings: FormatCodeSettings; - if (file) { - const info = this.getScriptInfoForNormalizedPath(file); - if (info) { - formatCodeSettings = info.getFormatCodeSettings(); - } - } - return formatCodeSettings || this.hostConfiguration.formatCodeOptions; + getFormatCodeOptions(file: NormalizedPath) { + const info = this.getScriptInfoForNormalizedPath(file); + return info && info.getFormatCodeSettings() || this.hostConfiguration.formatCodeOptions; + } + + getPreferences(file: NormalizedPath): UserPreferences { + const info = this.getScriptInfoForNormalizedPath(file); + return info && info.getPreferences() || this.hostConfiguration.preferences; } private onSourceFileChanged(fileName: string, eventKind: FileWatcherEventKind, path: Path) { @@ -1813,7 +1814,7 @@ namespace ts.server { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); if (info) { - info.setFormatOptions(convertFormatOptions(args.formatOptions)); + info.setOptions(convertFormatOptions(args.formatOptions), args.preferences); this.logger.info(`Host configuration update for file ${args.file}`); } } @@ -1826,6 +1827,9 @@ namespace ts.server { mergeMapLikes(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions)); this.logger.info("Format host information updated"); } + if (args.preferences) { + mergeMapLikes(this.hostConfiguration.preferences, args.preferences); + } if (args.extraFileExtensions) { this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; // We need to update the project structures again as it is possible that existing diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 47c607966006b..9e179db8e5e55 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1267,6 +1267,8 @@ namespace ts.server.protocol { */ formatOptions?: FormatCodeSettings; + preferences?: UserPreferences; + /** * The host's additional supported .js file extensions */ @@ -1742,15 +1744,13 @@ namespace ts.server.protocol { */ prefix?: string; /** - * If enabled, TypeScript will search through all external modules' exports and add them to the completions list. - * This affects lone identifier completions but not completions on the right hand side of `obj.`. + * @deprecated Use UserPreferences.includeCompletionsForModuleExports */ - includeExternalModuleExports: boolean; + includeExternalModuleExports?: boolean; /** - * If enabled, the completion list will include completions with invalid identifier names. - * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + * @deprecated Use UserPreferences.includeCompletionsWithInsertText */ - includeInsertTextCompletions: boolean; + includeInsertTextCompletions?: boolean; } /** @@ -2633,6 +2633,21 @@ namespace ts.server.protocol { insertSpaceBeforeTypeAnnotation?: boolean; } + export interface UserPreferences { + readonly quotePreference?: "double" | "single"; + /** + * If enabled, TypeScript will search through all external modules' exports and add them to the completions list. + * This affects lone identifier completions but not completions on the right hand side of `obj.`. + */ + readonly includeCompletionsForModuleExports?: boolean; + /** + * If enabled, the completion list will include completions with invalid identifier names. + * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + */ + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + } + export interface CompilerOptions { allowJs?: boolean; allowSyntheticDefaultImports?: boolean; diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index d368acd5ce3e4..843122e44c01f 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -209,7 +209,8 @@ namespace ts.server { * All projects that include this file */ readonly containingProjects: Project[] = []; - private formatCodeSettings: FormatCodeSettings; + private formatSettings: FormatCodeSettings | undefined; + private preferences: UserPreferences | undefined; /* @internal */ fileWatcher: FileWatcher; @@ -298,9 +299,8 @@ namespace ts.server { return this.realpath && this.realpath !== this.path ? this.realpath : undefined; } - getFormatCodeSettings() { - return this.formatCodeSettings; - } + getFormatCodeSettings(): FormatCodeSettings { return this.formatSettings; } + getPreferences(): UserPreferences { return this.preferences; } attachToProject(project: Project): boolean { const isNew = !this.isAttached(project); @@ -393,12 +393,19 @@ namespace ts.server { } } - setFormatOptions(formatSettings: FormatCodeSettings): void { + setOptions(formatSettings: FormatCodeSettings, preferences: UserPreferences): void { if (formatSettings) { - if (!this.formatCodeSettings) { - this.formatCodeSettings = getDefaultFormatCodeSettings(this.host); + if (!this.formatSettings) { + this.formatSettings = getDefaultFormatCodeSettings(this.host); + } + mergeMapLikes(this.formatSettings, formatSettings); + } + + if (preferences) { + if (!this.preferences) { + this.preferences = clone(defaultPreferences); } - mergeMapLikes(this.formatCodeSettings, formatSettings); + mergeMapLikes(this.preferences, preferences); } } diff --git a/src/server/session.ts b/src/server/session.ts index 6c3069ed0c160..1999360e34b06 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1125,7 +1125,7 @@ namespace ts.server { private getIndentation(args: protocol.IndentationRequestArgs) { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const position = this.getPositionInFile(args, file); - const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); + const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); const indentation = languageService.getIndentationAtPosition(file, position, options); return { position, indentation }; } @@ -1183,8 +1183,7 @@ namespace ts.server { const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); // TODO: avoid duplicate code (with formatonkey) - const edits = languageService.getFormattingEditsForRange(file, startPosition, endPosition, - this.projectService.getFormatCodeOptions(file)); + const edits = languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.getFormatOptions(file)); if (!edits) { return undefined; } @@ -1194,19 +1193,19 @@ namespace ts.server { private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); - const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); + const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); return languageService.getFormattingEditsForRange(file, args.position, args.endPosition, options); } private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); - const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); + const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); return languageService.getFormattingEditsForDocument(file, options); } private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); - const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); + const options = args.options ? convertFormatOptions(args.options) : this.getFormatOptions(file); return languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, options); } @@ -1214,7 +1213,7 @@ namespace ts.server { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); - const formatOptions = this.projectService.getFormatCodeOptions(file); + const formatOptions = this.getFormatOptions(file); const edits = languageService.getFormattingEditsAfterKeystroke(file, position, args.key, formatOptions); // Check whether we should auto-indent. This will be when @@ -1270,7 +1269,11 @@ namespace ts.server { const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); - const completions = project.getLanguageService().getCompletionsAtPosition(file, position, args); + const completions = project.getLanguageService().getCompletionsAtPosition(file, position, { + ...this.getPreferences(file), + includeExternalModuleExports: args.includeExternalModuleExports, + includeInsertTextCompletions: args.includeInsertTextCompletions + }); if (simplifiedResult) { return mapDefined(completions && completions.entries, entry => { if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) { @@ -1294,7 +1297,7 @@ namespace ts.server { const result = mapDefined(args.entryNames, entryName => { const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName; - return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source); + return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file)); }); return simplifiedResult ? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(project, action)) })) @@ -1592,7 +1595,7 @@ namespace ts.server { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const { position, textRange } = this.extractPositionAndRange(args, scriptInfo); - return project.getLanguageService().getApplicableRefactors(file, position || textRange); + return project.getLanguageService().getApplicableRefactors(file, position || textRange, this.getPreferences(file)); } private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { @@ -1602,10 +1605,11 @@ namespace ts.server { const result = project.getLanguageService().getEditsForRefactor( file, - this.projectService.getFormatCodeOptions(file), + this.getFormatOptions(file), position || textRange, args.refactor, - args.action + args.action, + this.getPreferences(file), ); if (result === undefined) { @@ -1631,8 +1635,7 @@ namespace ts.server { private organizeImports({ scope }: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { Debug.assert(scope.type === "file"); const { file, project } = this.getFileAndProject(scope.args); - const formatOptions = this.projectService.getFormatCodeOptions(file); - const changes = project.getLanguageService().organizeImports({ type: "file", fileName: file }, formatOptions); + const changes = project.getLanguageService().organizeImports({ type: "file", fileName: file }, this.getFormatOptions(file), this.getPreferences(file)); if (simplifiedResult) { return this.mapTextChangesToCodeEdits(project, changes); } @@ -1649,9 +1652,8 @@ namespace ts.server { const scriptInfo = project.getScriptInfoForNormalizedPath(file); const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); - const formatOptions = this.projectService.getFormatCodeOptions(file); - const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, formatOptions); + const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file)); if (!codeActions) { return undefined; } @@ -1666,8 +1668,7 @@ namespace ts.server { private getCombinedCodeFix({ scope, fixId }: protocol.GetCombinedCodeFixRequestArgs, simplifiedResult: boolean): protocol.CombinedCodeActions | CombinedCodeActions { Debug.assert(scope.type === "file"); const { file, project } = this.getFileAndProject(scope.args); - const formatOptions = this.projectService.getFormatCodeOptions(file); - const res = project.getLanguageService().getCombinedCodeFix({ type: "file", fileName: file }, fixId, formatOptions); + const res = project.getLanguageService().getCombinedCodeFix({ type: "file", fileName: file }, fixId, this.getFormatOptions(file), this.getPreferences(file)); if (simplifiedResult) { return { changes: this.mapTextChangesToCodeEdits(project, res.changes), commands: res.commands }; } @@ -2188,6 +2189,14 @@ namespace ts.server { "Error processing request. " + (err).message + "\n" + (err).stack); } } + + private getFormatOptions(file: NormalizedPath): FormatCodeSettings { + return this.projectService.getFormatCodeOptions(file); + } + + private getPreferences(file: NormalizedPath): UserPreferences { + return this.projectService.getPreferences(file); + } } export interface HandlerResponse { diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index cc503ada0ffb6..74d3487a74c8d 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -11,6 +11,7 @@ namespace ts { sourceFile: SourceFile; program: Program; cancellationToken: CancellationToken; + preferences: UserPreferences; } export interface CodeFixAllContext extends CodeFixContextBase { diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 7dbbc39843fcb..b315dd0659fae 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -11,7 +11,7 @@ namespace ts.codefix { const info = getInfo(context.sourceFile, context.span.start, context.program.getTypeChecker()); if (!info) return undefined; const { classDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info; - const methodCodeAction = call && getActionForMethodDeclaration(context, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs); + const methodCodeAction = call && getActionForMethodDeclaration(context, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs, context.preferences); const addMember = inJs ? singleElementArray(getActionsForAddMissingMemberInJavaScriptFile(context, classDeclarationSourceFile, classDeclaration, token.text, makeStatic)) : getActionsForAddMissingMemberInTypeScriptFile(context, classDeclarationSourceFile, classDeclaration, token, makeStatic); @@ -21,7 +21,7 @@ namespace ts.codefix { getAllCodeActions: context => { const seenNames = createMap(); return codeFixAll(context, errorCodes, (changes, diag) => { - const { program } = context; + const { program, preferences } = context; const info = getInfo(diag.file!, diag.start!, program.getTypeChecker()); if (!info) return; const { classDeclaration, classDeclarationSourceFile, inJs, makeStatic, token, call } = info; @@ -31,7 +31,7 @@ namespace ts.codefix { // Always prefer to add a method declaration if possible. if (call) { - addMethodDeclaration(changes, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs); + addMethodDeclaration(changes, classDeclarationSourceFile, classDeclaration, token, call, makeStatic, inJs, preferences); } else { if (inJs) { @@ -181,14 +181,32 @@ namespace ts.codefix { return { description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_property_0), [tokenName]), changes, fixId: undefined }; } - function getActionForMethodDeclaration(context: CodeFixContext, classDeclarationSourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, token: Identifier, callExpression: CallExpression, makeStatic: boolean, inJs: boolean): CodeFixAction | undefined { + function getActionForMethodDeclaration( + context: CodeFixContext, + classDeclarationSourceFile: SourceFile, + classDeclaration: ClassLikeDeclaration, + token: Identifier, + callExpression: CallExpression, + makeStatic: boolean, + inJs: boolean, + preferences: UserPreferences, + ): CodeFixAction | undefined { const description = formatStringFromArgs(getLocaleSpecificMessage(makeStatic ? Diagnostics.Declare_static_method_0 : Diagnostics.Declare_method_0), [token.text]); - const changes = textChanges.ChangeTracker.with(context, t => addMethodDeclaration(t, classDeclarationSourceFile, classDeclaration, token, callExpression, makeStatic, inJs)); + const changes = textChanges.ChangeTracker.with(context, t => addMethodDeclaration(t, classDeclarationSourceFile, classDeclaration, token, callExpression, makeStatic, inJs, preferences)); return { description, changes, fixId }; } - function addMethodDeclaration(changeTracker: textChanges.ChangeTracker, classDeclarationSourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, token: Identifier, callExpression: CallExpression, makeStatic: boolean, inJs: boolean) { - const methodDeclaration = createMethodFromCallExpression(callExpression, token.text, inJs, makeStatic); + function addMethodDeclaration( + changeTracker: textChanges.ChangeTracker, + classDeclarationSourceFile: SourceFile, + classDeclaration: ClassLikeDeclaration, + token: Identifier, + callExpression: CallExpression, + makeStatic: boolean, + inJs: boolean, + preferences: UserPreferences, + ): void { + const methodDeclaration = createMethodFromCallExpression(callExpression, token.text, inJs, makeStatic, preferences); changeTracker.insertNodeAtClassStart(classDeclarationSourceFile, classDeclaration, methodDeclaration); } } diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index 786d61d798d80..cb33d1c1793ab 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -10,7 +10,7 @@ namespace ts.codefix { getCodeActions(context) { const { program, sourceFile, span } = context; const changes = textChanges.ChangeTracker.with(context, t => - addMissingMembers(getClass(sourceFile, span.start), sourceFile, program.getTypeChecker(), t)); + addMissingMembers(getClass(sourceFile, span.start), sourceFile, program.getTypeChecker(), t, context.preferences)); return changes.length === 0 ? undefined : [{ description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), changes, fixId }]; }, fixIds: [fixId], @@ -19,7 +19,7 @@ namespace ts.codefix { return codeFixAll(context, errorCodes, (changes, diag) => { const classDeclaration = getClass(diag.file!, diag.start!); if (addToSeen(seenClassDeclarations, getNodeId(classDeclaration))) { - addMissingMembers(classDeclaration, context.sourceFile, context.program.getTypeChecker(), changes); + addMissingMembers(classDeclaration, context.sourceFile, context.program.getTypeChecker(), changes, context.preferences); } }); }, @@ -32,7 +32,7 @@ namespace ts.codefix { return cast(token.parent, isClassLike); } - function addMissingMembers(classDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, checker: TypeChecker, changeTracker: textChanges.ChangeTracker): void { + function addMissingMembers(classDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, checker: TypeChecker, changeTracker: textChanges.ChangeTracker, preferences: UserPreferences): void { const extendsNode = getClassExtendsHeritageClauseElement(classDeclaration); const instantiatedExtendsType = checker.getTypeAtLocation(extendsNode); @@ -40,7 +40,7 @@ namespace ts.codefix { // so duplicates cannot occur. const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember); - createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, checker, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, checker, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); } function symbolPointsToNonPrivateAndAbstractMember(symbol: Symbol): boolean { diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index c295d4fb3c73f..38cedbb2ce48f 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -10,7 +10,7 @@ namespace ts.codefix { const classDeclaration = getClass(sourceFile, span.start); const checker = program.getTypeChecker(); return mapDefined(getClassImplementsHeritageClauseElements(classDeclaration), implementedTypeNode => { - const changes = textChanges.ChangeTracker.with(context, t => addMissingDeclarations(checker, implementedTypeNode, sourceFile, classDeclaration, t)); + const changes = textChanges.ChangeTracker.with(context, t => addMissingDeclarations(checker, implementedTypeNode, sourceFile, classDeclaration, t, context.preferences)); if (changes.length === 0) return undefined; const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Implement_interface_0), [implementedTypeNode.getText()]); return { description, changes, fixId }; @@ -23,7 +23,7 @@ namespace ts.codefix { const classDeclaration = getClass(diag.file!, diag.start!); if (addToSeen(seenClassDeclarations, getNodeId(classDeclaration))) { for (const implementedTypeNode of getClassImplementsHeritageClauseElements(classDeclaration)) { - addMissingDeclarations(context.program.getTypeChecker(), implementedTypeNode, diag.file!, classDeclaration, changes); + addMissingDeclarations(context.program.getTypeChecker(), implementedTypeNode, diag.file!, classDeclaration, changes, context.preferences); } } }); @@ -39,7 +39,8 @@ namespace ts.codefix { implementedTypeNode: ExpressionWithTypeArguments, sourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, - changeTracker: textChanges.ChangeTracker + changeTracker: textChanges.ChangeTracker, + preferences: UserPreferences, ): void { // Note that this is ultimately derived from a map indexed by symbol names, // so duplicates cannot occur. @@ -56,7 +57,7 @@ namespace ts.codefix { createMissingIndexSignatureDeclaration(implementedType, IndexKind.String); } - createMissingMemberNodes(classDeclaration, nonPrivateMembers, checker, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + createMissingMemberNodes(classDeclaration, nonPrivateMembers, checker, preferences, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void { const indexInfoOfKind = checker.getIndexInfoOfType(type, kind); diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 6a45c66ca816d..35e75d831fdc8 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -6,11 +6,11 @@ namespace ts.codefix { * @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for. * @returns Empty string iff there are no member insertions. */ - export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray, checker: TypeChecker, out: (node: ClassElement) => void): void { + export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: ReadonlyArray, checker: TypeChecker, preferences: UserPreferences, out: (node: ClassElement) => void): void { const classMembers = classDeclaration.symbol.members; for (const symbol of possiblyMissingSymbols) { if (!classMembers.has(symbol.escapedName)) { - addNewNodeForMemberSymbol(symbol, classDeclaration, checker, out); + addNewNodeForMemberSymbol(symbol, classDeclaration, checker, preferences, out); } } } @@ -18,7 +18,7 @@ namespace ts.codefix { /** * @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`. */ - function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, checker: TypeChecker, out: (node: Node) => void): void { + function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, checker: TypeChecker, preferences: UserPreferences, out: (node: Node) => void): void { const declarations = symbol.getDeclarations(); if (!(declarations && declarations.length)) { return undefined; @@ -63,7 +63,7 @@ namespace ts.codefix { if (declarations.length === 1) { Debug.assert(signatures.length === 1); const signature = signatures[0]; - outputMethod(signature, modifiers, name, createStubbedMethodBody()); + outputMethod(signature, modifiers, name, createStubbedMethodBody(preferences)); break; } @@ -74,11 +74,11 @@ namespace ts.codefix { if (declarations.length > signatures.length) { const signature = checker.getSignatureFromDeclaration(declarations[declarations.length - 1] as SignatureDeclaration); - outputMethod(signature, modifiers, name, createStubbedMethodBody()); + outputMethod(signature, modifiers, name, createStubbedMethodBody(preferences)); } else { Debug.assert(declarations.length === signatures.length); - out(createMethodImplementingSignatures(signatures, name, optional, modifiers)); + out(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences)); } break; } @@ -107,7 +107,13 @@ namespace ts.codefix { return nodes && createNodeArray(nodes.map(getSynthesizedDeepClone)); } - export function createMethodFromCallExpression({ typeArguments, arguments: args }: CallExpression, methodName: string, inJs: boolean, makeStatic: boolean): MethodDeclaration { + export function createMethodFromCallExpression( + { typeArguments, arguments: args }: CallExpression, + methodName: string, + inJs: boolean, + makeStatic: boolean, + preferences: UserPreferences, + ): MethodDeclaration { return createMethod( /*decorators*/ undefined, /*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, @@ -118,7 +124,7 @@ namespace ts.codefix { createTypeParameterDeclaration(CharacterCodes.T + typeArguments.length - 1 <= CharacterCodes.Z ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`)), /*parameters*/ createDummyParameters(args.length, /*names*/ undefined, /*minArgumentCount*/ undefined, inJs), /*type*/ inJs ? undefined : createKeywordTypeNode(SyntaxKind.AnyKeyword), - createStubbedMethodBody()); + createStubbedMethodBody(preferences)); } function createDummyParameters(argCount: number, names: string[] | undefined, minArgumentCount: number | undefined, inJs: boolean): ParameterDeclaration[] { @@ -137,7 +143,13 @@ namespace ts.codefix { return parameters; } - function createMethodImplementingSignatures(signatures: ReadonlyArray, name: PropertyName, optional: boolean, modifiers: ReadonlyArray | undefined): MethodDeclaration { + function createMethodImplementingSignatures( + signatures: ReadonlyArray, + name: PropertyName, + optional: boolean, + modifiers: ReadonlyArray | undefined, + preferences: UserPreferences, + ): MethodDeclaration { /** This is *a* signature with the maximal number of arguments, * such that if there is a "maximal" signature without rest arguments, * this is one of them. @@ -178,7 +190,8 @@ namespace ts.codefix { optional, /*typeParameters*/ undefined, parameters, - /*returnType*/ undefined); + /*returnType*/ undefined, + preferences); } function createStubbedMethod( @@ -187,7 +200,9 @@ namespace ts.codefix { optional: boolean, typeParameters: ReadonlyArray | undefined, parameters: ReadonlyArray, - returnType: TypeNode | undefined) { + returnType: TypeNode | undefined, + preferences: UserPreferences + ): MethodDeclaration { return createMethod( /*decorators*/ undefined, modifiers, @@ -197,16 +212,16 @@ namespace ts.codefix { typeParameters, parameters, returnType, - createStubbedMethodBody()); + createStubbedMethodBody(preferences)); } - function createStubbedMethodBody() { + function createStubbedMethodBody(preferences: UserPreferences): Block { return createBlock( [createThrow( createNew( createIdentifier("Error"), /*typeArguments*/ undefined, - [createLiteral("Method not implemented.")]))], + [createLiteral("Method not implemented.", /*isSingleQuote*/ preferences.quotePreference === "single")]))], /*multiline*/ true); } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index b571e7a990268..f51e59f327c6e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -30,6 +30,7 @@ namespace ts.codefix { compilerOptions: CompilerOptions; getCanonicalFileName: GetCanonicalFileName; cachedImportDeclarations?: ImportDeclarationMap; + preferences: UserPreferences; } function createCodeAction(descriptionDiagnostic: DiagnosticMessage, diagnosticArgs: string[], changes: FileTextChanges[]): CodeFixAction { @@ -53,7 +54,8 @@ namespace ts.codefix { cachedImportDeclarations: [], getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames), symbolName, - symbolToken + symbolToken, + preferences: context.preferences, }; } @@ -95,12 +97,13 @@ namespace ts.codefix { formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, symbolToken: Node | undefined, + preferences: UserPreferences, ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { const exportInfos = getAllReExportingModules(exportedSymbol, checker, allSourceFiles); Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol)); // We sort the best codefixes first, so taking `first` is best for completions. - const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, compilerOptions, getCanonicalFileName, host)).moduleSpecifier; - const ctx: ImportCodeFixContext = { host, program, checker, compilerOptions, sourceFile, formatContext, symbolName, getCanonicalFileName, symbolToken }; + const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, compilerOptions, getCanonicalFileName, host, preferences)).moduleSpecifier; + const ctx: ImportCodeFixContext = { host, program, checker, compilerOptions, sourceFile, formatContext, symbolName, getCanonicalFileName, symbolToken, preferences }; return { moduleSpecifier, codeAction: first(getCodeActionsForImport(exportInfos, ctx)) }; } function getAllReExportingModules(exportedSymbol: Symbol, checker: TypeChecker, allSourceFiles: ReadonlyArray): ReadonlyArray { @@ -167,12 +170,12 @@ namespace ts.codefix { return cached; } - function getCodeActionForNewImport(context: SymbolContext, { moduleSpecifier, importKind }: NewImportInfo): CodeFixAction { - const { sourceFile, symbolName } = context; + function getCodeActionForNewImport(context: SymbolContext & { preferences: UserPreferences }, { moduleSpecifier, importKind }: NewImportInfo): CodeFixAction { + const { sourceFile, symbolName, preferences } = context; const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax); const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); - const quotedModuleSpecifier = createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes); + const quotedModuleSpecifier = createLiteral(moduleSpecifierWithoutQuotes, shouldUseSingleQuote(sourceFile, preferences)); const importDecl = importKind !== ImportKind.Equals ? createImportDeclaration( /*decorators*/ undefined, @@ -200,11 +203,14 @@ namespace ts.codefix { return createCodeAction(Diagnostics.Import_0_from_module_1, [symbolName, moduleSpecifierWithoutQuotes], changes); } - function createStringLiteralWithQuoteStyle(sourceFile: SourceFile, text: string): StringLiteral { - const literal = createLiteral(text); - const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); - literal.singleQuote = !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile); - return literal; + function shouldUseSingleQuote(sourceFile: SourceFile, preferences: UserPreferences): boolean { + if (preferences.quotePreference) { + return preferences.quotePreference === "single"; + } + else { + const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); + return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile); + } } function usesJsExtensionOnImports(sourceFile: SourceFile): boolean { @@ -229,25 +235,26 @@ namespace ts.codefix { program: Program, sourceFile: SourceFile, moduleSymbols: ReadonlyArray, - options: CompilerOptions, + compilerOptions: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost, + preferences: UserPreferences, ): ReadonlyArray { - const { baseUrl, paths, rootDirs } = options; + const { baseUrl, paths, rootDirs } = compilerOptions; const addJsExtension = usesJsExtensionOnImports(sourceFile); const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind }) => { const modulePathsGroups = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => { const sourceDirectory = getDirectoryPath(sourceFile.fileName); const global = tryGetModuleNameFromAmbientModule(moduleSymbol) - || tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName, addJsExtension) - || tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) + || tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) + || tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory) || rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName); if (global) { return [global]; } - const relativePath = removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), options, addJsExtension); - if (!baseUrl) { + const relativePath = removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), compilerOptions, addJsExtension); + if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { return [relativePath]; } @@ -256,7 +263,7 @@ namespace ts.codefix { return [relativePath]; } - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, options, addJsExtension); + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, compilerOptions, addJsExtension); if (paths) { const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); if (fromPaths) { @@ -264,6 +271,12 @@ namespace ts.codefix { } } + if (preferences.importModuleSpecifierPreference === "non-relative") { + return [importRelativeToBaseUrl]; + } + + if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); + if (isPathRelativeToParent(relativeToBaseUrl)) { return [relativePath]; } @@ -559,7 +572,7 @@ namespace ts.codefix { const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier); const newImportInfos = existingDeclaration ? [existingDeclaration] - : getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host); + : getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host, ctx.preferences); return newImportInfos.map(info => getCodeActionForNewImport(ctx, info)); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 7d607b6618783..7e090b3ccd430 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -32,7 +32,7 @@ namespace ts.Completions { sourceFile: SourceFile, position: number, allSourceFiles: ReadonlyArray, - options: GetCompletionsAtPositionOptions, + preferences: UserPreferences, ): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host); @@ -44,7 +44,7 @@ namespace ts.Completions { if (isInString(sourceFile, position, contextToken)) { return !contextToken || !isStringLiteralLike(contextToken) ? undefined - : convertStringLiteralCompletions(getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host), sourceFile, typeChecker, log); + : convertStringLiteralCompletions(getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host), sourceFile, typeChecker, log, preferences); } if (contextToken && isBreakOrContinueStatement(contextToken.parent) @@ -52,14 +52,14 @@ namespace ts.Completions { return getLabelCompletionAtPosition(contextToken.parent); } - const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, options, compilerOptions.target); + const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, preferences, compilerOptions.target); if (!completionData) { return undefined; } switch (completionData.kind) { case CompletionDataKind.Data: - return completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, options.includeInsertTextCompletions); + return completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, preferences); case CompletionDataKind.JsDocTagName: // If the current position is a jsDoc tag name, only tag names should be provided for completion return jsdocCompletionInfo(JsDoc.getJSDocTagNameCompletions()); @@ -73,7 +73,7 @@ namespace ts.Completions { } } - function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log): CompletionInfo | undefined { + function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined { if (completion === undefined) { return undefined; } @@ -82,7 +82,7 @@ namespace ts.Completions { return convertPathCompletions(completion.paths); case StringLiteralCompletionKind.Properties: { const entries: CompletionEntry[] = []; - getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String); // Target will not be used, so arbitrary + getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String, preferences); // Target will not be used, so arbitrary return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries }; } case StringLiteralCompletionKind.Types: { @@ -105,7 +105,7 @@ namespace ts.Completions { return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; } - function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, includeInsertTextCompletions: boolean): CompletionInfo { + function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo { const { symbols, completionKind, isInSnippetScope, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) { @@ -127,7 +127,7 @@ namespace ts.Completions { const entries: CompletionEntry[] = []; if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries); } else { @@ -135,7 +135,7 @@ namespace ts.Completions { return undefined; } - getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); + getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); } // TODO add filter for keyword based on type/value/namespace and also location @@ -196,7 +196,7 @@ namespace ts.Completions { recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer, - includeInsertTextCompletions: boolean, + preferences: UserPreferences, ): CompletionEntry | undefined { const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind); if (!info) { @@ -206,12 +206,12 @@ namespace ts.Completions { let insertText: string | undefined; let replacementSpan: TextSpan | undefined; - if (includeInsertTextCompletions) { + if (preferences.includeCompletionsWithInsertText) { if (origin && origin.type === "this-type") { - insertText = needsConvertPropertyAccess ? `this[${quote(name)}]` : `this.${name}`; + insertText = needsConvertPropertyAccess ? `this[${quote(name, preferences)}]` : `this.${name}`; } else if (needsConvertPropertyAccess) { - insertText = `[${quote(name)}]`; + insertText = `[${quote(name, preferences)}]`; const dot = findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!; // If the text after the '.' starts with this name, write over it. Else, add new text. const end = startsWith(name, propertyAccessToConvert!.name.text) ? propertyAccessToConvert!.name.end : dot.end; @@ -227,7 +227,7 @@ namespace ts.Completions { } } - if (insertText !== undefined && !includeInsertTextCompletions) { + if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { return undefined; } @@ -252,9 +252,17 @@ namespace ts.Completions { }; } - function quote(text: string): string { - // TODO: GH#20619 Use configured quote style - return JSON.stringify(text); + function quote(text: string, preferences: UserPreferences): string { + const quoted = JSON.stringify(text); + switch (preferences.quotePreference) { + case undefined: + case "double": + return quoted; + case "single": + return `'${stripQuotes(quoted).replace("'", "\\'").replace('\\"', '"')}'`; + default: + return Debug.assertNever(preferences.quotePreference); + } } function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol, checker: TypeChecker): boolean { @@ -279,7 +287,7 @@ namespace ts.Completions { target: ScriptTarget, log: Log, kind: CompletionKind, - includeInsertTextCompletions?: boolean, + preferences: UserPreferences, propertyAccessToConvert?: PropertyAccessExpression | undefined, isJsxInitializer?: IsJsxInitializer, recommendedCompletion?: Symbol, @@ -293,7 +301,7 @@ namespace ts.Completions { const uniques = createMap(); for (const symbol of symbols) { const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, includeInsertTextCompletions); + const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, preferences); if (!entry) { continue; } @@ -471,7 +479,7 @@ namespace ts.Completions { { name, source }: CompletionEntryIdentifier, allSourceFiles: ReadonlyArray, ): SymbolCompletion | { type: "request", request: Request } | { type: "none" } { - const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeExternalModuleExports: true, includeInsertTextCompletions: true }, compilerOptions.target); + const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, compilerOptions.target); if (!completionData) { return { type: "none" }; } @@ -516,6 +524,7 @@ namespace ts.Completions { host: LanguageServiceHost, formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, + preferences: UserPreferences, ): CompletionEntryDetails { const typeChecker = program.getTypeChecker(); const { name } = entryId; @@ -537,7 +546,7 @@ namespace ts.Completions { } case "symbol": { const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles); + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles, preferences); const kindModifiers = SymbolDisplay.getSymbolModifiers(symbol); const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All); return { name, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions, source: sourceDisplay }; @@ -577,26 +586,13 @@ namespace ts.Completions { formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, allSourceFiles: ReadonlyArray, + preferences: UserPreferences, ): CodeActionsAndSourceDisplay { const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; - return symbolOriginInfo && symbolOriginInfo.type === "export" - ? getCodeActionsAndSourceDisplayForImport(symbolOriginInfo, symbol, program, checker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles) - : { codeActions: undefined, sourceDisplay: undefined }; - } + if (!symbolOriginInfo || symbolOriginInfo.type !== "export") { + return { codeActions: undefined, sourceDisplay: undefined }; + } - function getCodeActionsAndSourceDisplayForImport( - symbolOriginInfo: SymbolOriginInfoExport, - symbol: Symbol, - program: Program, - checker: TypeChecker, - host: LanguageServiceHost, - compilerOptions: CompilerOptions, - sourceFile: SourceFile, - previousToken: Node, - formatContext: formatting.FormatContext, - getCanonicalFileName: GetCanonicalFileName, - allSourceFiles: ReadonlyArray - ): CodeActionsAndSourceDisplay { const { moduleSymbol } = symbolOriginInfo; const exportedSymbol = skipAlias(symbol.exportSymbol || symbol, checker); const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction( @@ -611,7 +607,8 @@ namespace ts.Completions { allSourceFiles, formatContext, getCanonicalFileName, - previousToken); + previousToken, + preferences); return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] }; } @@ -738,7 +735,7 @@ namespace ts.Completions { sourceFile: SourceFile, position: number, allSourceFiles: ReadonlyArray, - options: GetCompletionsAtPositionOptions, + preferences: Pick, target: ScriptTarget, ): CompletionData | Request | undefined { let start = timestamp(); @@ -1143,7 +1140,7 @@ namespace ts.Completions { symbols = Debug.assertEachDefined(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings), "getSymbolsInScope() should all be defined"); // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` - if (options.includeInsertTextCompletions && scopeNode.kind !== SyntaxKind.SourceFile) { + if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) { const thisType = typeChecker.tryGetThisTypeAt(scopeNode); if (thisType) { for (const symbol of getPropertiesForCompletion(thisType, typeChecker, /*isForAccess*/ true)) { @@ -1154,7 +1151,7 @@ namespace ts.Completions { } // Don't suggest import completions for a commonjs-only module - if (options.includeExternalModuleExports && !(sourceFile.commonJsModuleIndicator && !sourceFile.externalModuleIndicator)) { + if (preferences.includeCompletionsForModuleExports && !(sourceFile.commonJsModuleIndicator && !sourceFile.externalModuleIndicator)) { getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target); } filterGlobalCompletion(symbols); diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 511fa821fd231..f6489fce6d208 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -11,7 +11,9 @@ namespace ts.OrganizeImports { sourceFile: SourceFile, formatContext: formatting.FormatContext, host: LanguageServiceHost, - program: Program) { + program: Program, + _preferences: UserPreferences, + ) { const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext }); diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 3d5957c694c99..f3504ed89b82d 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -14,6 +14,7 @@ namespace ts { endPosition?: number; program: Program; cancellationToken?: CancellationToken; + preferences: UserPreferences; } export namespace refactor { diff --git a/src/services/services.ts b/src/services/services.ts index 894447b9bedcb..d498b5d911591 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -33,7 +33,7 @@ namespace ts { /** The version of the language service API */ - export const servicesVersion = "0.7"; + export const servicesVersion = "0.8"; function createNode(kind: TKind, pos: number, end: number, parent?: Node): NodeObject | TokenObject | IdentifierObject { const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) : @@ -1430,7 +1430,13 @@ namespace ts { return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; } - function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = { includeExternalModuleExports: false, includeInsertTextCompletions: false }): CompletionInfo { + function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = defaultPreferences): CompletionInfo { + // Convert from deprecated options names to new names + const fullPreferences: UserPreferences = { + ...identity(options), // avoid excess property check + includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports, + includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions, + }; synchronizeHostData(); return Completions.getCompletionsAtPosition( host, @@ -1440,10 +1446,10 @@ namespace ts { getValidSourceFile(fileName), position, program.getSourceFiles(), - options); + fullPreferences); } - function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions?: FormatCodeSettings, source?: string): CompletionEntryDetails { + function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = defaultPreferences): CompletionEntryDetails { synchronizeHostData(); return Completions.getCompletionEntryDetails( program, @@ -1455,7 +1461,8 @@ namespace ts { program.getSourceFiles(), host, formattingOptions && formatting.getFormatContext(formattingOptions), - getCanonicalFileName); + getCanonicalFileName, + preferences); } function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol { @@ -1820,7 +1827,7 @@ namespace ts { return []; } - function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings): ReadonlyArray { + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const span = createTextSpanFromBounds(start, end); @@ -1828,26 +1835,26 @@ namespace ts { return flatMap(deduplicate(errorCodes, equateValues, compareValues), errorCode => { cancellationToken.throwIfCancellationRequested(); - return codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext }); + return codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences }); }); } - function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings): CombinedCodeActions { + function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): CombinedCodeActions { synchronizeHostData(); Debug.assert(scope.type === "file"); const sourceFile = getValidSourceFile(scope.fileName); const formatContext = formatting.getFormatContext(formatOptions); - return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext }); + return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); } - function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray { + function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { synchronizeHostData(); Debug.assert(scope.type === "file"); const sourceFile = getValidSourceFile(scope.fileName); const formatContext = formatting.getFormatContext(formatOptions); - return OrganizeImports.organizeImports(sourceFile, formatContext, host, program); + return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } function applyCodeActionCommand(action: CodeActionCommand): Promise; @@ -2071,7 +2078,7 @@ namespace ts { return Rename.getRenameInfo(program.getTypeChecker(), defaultLibFileName, getCanonicalFileName, getValidSourceFile(fileName), position); } - function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, formatOptions?: FormatCodeSettings): RefactorContext { + function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings): RefactorContext { const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; return { file, @@ -2081,13 +2088,14 @@ namespace ts { host, formatContext: formatting.getFormatContext(formatOptions), cancellationToken, + preferences, }; } - function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = defaultPreferences): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange)); + return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences)); } function getEditsForRefactor( @@ -2095,11 +2103,13 @@ namespace ts { formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, - actionName: string): RefactorEditInfo { + actionName: string, + preferences: UserPreferences = defaultPreferences, + ): RefactorEditInfo { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, formatOptions), refactorName, actionName); + return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); } return { diff --git a/src/services/shims.ts b/src/services/shims.ts index 1a43fbda14610..42311843ea10e 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -152,8 +152,8 @@ namespace ts { getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string; getEncodedSemanticClassifications(fileName: string, start: number, length: number): string; - getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): string; - getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined): string; + getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined): string; getQuickInfoAtPosition(fileName: string, position: number): string; @@ -913,20 +913,20 @@ namespace ts { * to provide at the given source position and providing a member completion * list if requested. */ - public getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined) { + public getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined) { return this.forwardJSONCall( - `getCompletionsAtPosition('${fileName}', ${position}, ${options})`, - () => this.languageService.getCompletionsAtPosition(fileName, position, options) + `getCompletionsAtPosition('${fileName}', ${position}, ${preferences})`, + () => this.languageService.getCompletionsAtPosition(fileName, position, preferences) ); } /** Get a string based representation of a completion list entry details */ - public getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined) { + public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined) { return this.forwardJSONCall( `getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, () => { - const localOptions: FormatCodeOptions = options === undefined ? undefined : JSON.parse(options); - return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source); + const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); + return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences); } ); } diff --git a/src/services/types.ts b/src/services/types.ts index dba10960cdf76..f37d3cb592c8b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -214,6 +214,15 @@ namespace ts { installPackage?(options: InstallPackageOptions): Promise; } + export interface UserPreferences { + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + } + /* @internal */ + export const defaultPreferences: UserPreferences = {}; + // // Public services of a language service instance associated // with a language service host instance @@ -249,8 +258,9 @@ namespace ts { fileName: string, position: number, name: string, - options: FormatCodeOptions | FormatCodeSettings | undefined, + formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, + preferences: UserPreferences | undefined, ): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol; @@ -296,8 +306,8 @@ namespace ts { getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings): ReadonlyArray; - getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings): CombinedCodeActions; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray; + getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; applyCodeActionCommand(action: CodeActionCommand): Promise; applyCodeActionCommand(action: CodeActionCommand[]): Promise; applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; @@ -307,9 +317,9 @@ namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; - organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray; + getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; @@ -330,9 +340,12 @@ namespace ts { export type OrganizeImportsScope = CombinedCodeFixScope; - export interface GetCompletionsAtPositionOptions { - includeExternalModuleExports: boolean; - includeInsertTextCompletions: boolean; + /** @deprecated Use UserPreferences */ + export interface GetCompletionsAtPositionOptions extends UserPreferences { + /** @deprecated Use includeCompletionsForModuleExports */ + includeExternalModuleExports?: boolean; + /** @deprecated Use includeCompletionsWithInsertText */ + includeInsertTextCompletions?: boolean; } export interface ApplyCodeActionCommandResult { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 4bc0f5160d2cc..82716f54f6e19 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4072,6 +4072,12 @@ declare namespace ts { isKnownTypesPackageName?(name: string): boolean; installPackage?(options: InstallPackageOptions): Promise; } + interface UserPreferences { + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + } interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): Diagnostic[]; @@ -4089,7 +4095,7 @@ declare namespace ts { getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo; - getCompletionEntryDetails(fileName: string, position: number, name: string, options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails; + getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan; @@ -4119,8 +4125,8 @@ declare namespace ts { getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion; isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings): ReadonlyArray; - getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings): CombinedCodeActions; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray; + getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; applyCodeActionCommand(action: CodeActionCommand): Promise; applyCodeActionCommand(action: CodeActionCommand[]): Promise; applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; @@ -4130,9 +4136,9 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; - organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray; + getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; dispose(): void; @@ -4142,9 +4148,12 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - interface GetCompletionsAtPositionOptions { - includeExternalModuleExports: boolean; - includeInsertTextCompletions: boolean; + /** @deprecated Use UserPreferences */ + interface GetCompletionsAtPositionOptions extends UserPreferences { + /** @deprecated Use includeCompletionsForModuleExports */ + includeExternalModuleExports?: boolean; + /** @deprecated Use includeCompletionsWithInsertText */ + includeInsertTextCompletions?: boolean; } interface ApplyCodeActionCommandResult { successMessage: string; @@ -4813,7 +4822,7 @@ declare namespace ts { } declare namespace ts { /** The version of the language service API */ - const servicesVersion = "0.7"; + const servicesVersion = "0.8"; function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings; function displayPartsToString(displayParts: SymbolDisplayPart[]): string; function getDefaultCompilerOptions(): CompilerOptions; @@ -5961,6 +5970,7 @@ declare namespace ts.server.protocol { * The format options to use during formatting and other code editing features. */ formatOptions?: FormatCodeSettings; + preferences?: UserPreferences; /** * The host's additional supported .js file extensions */ @@ -6333,15 +6343,13 @@ declare namespace ts.server.protocol { */ prefix?: string; /** - * If enabled, TypeScript will search through all external modules' exports and add them to the completions list. - * This affects lone identifier completions but not completions on the right hand side of `obj.`. + * @deprecated Use UserPreferences.includeCompletionsForModuleExports */ - includeExternalModuleExports: boolean; + includeExternalModuleExports?: boolean; /** - * If enabled, the completion list will include completions with invalid identifier names. - * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + * @deprecated Use UserPreferences.includeCompletionsWithInsertText */ - includeInsertTextCompletions: boolean; + includeInsertTextCompletions?: boolean; } /** * Completions request; value of command field is "completions". @@ -7098,6 +7106,20 @@ declare namespace ts.server.protocol { placeOpenBraceOnNewLineForControlBlocks?: boolean; insertSpaceBeforeTypeAnnotation?: boolean; } + interface UserPreferences { + readonly quotePreference?: "double" | "single"; + /** + * If enabled, TypeScript will search through all external modules' exports and add them to the completions list. + * This affects lone identifier completions but not completions on the right hand side of `obj.`. + */ + readonly includeCompletionsForModuleExports?: boolean; + /** + * If enabled, the completion list will include completions with invalid identifier names. + * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + */ + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + } interface CompilerOptions { allowJs?: boolean; allowSyntheticDefaultImports?: boolean; @@ -7363,6 +7385,8 @@ declare namespace ts.server { executeWithRequestId(requestId: number, f: () => T): T; executeCommand(request: protocol.Request): HandlerResponse; onMessage(message: string): void; + private getFormatOptions; + private getPreferences; } interface HandlerResponse { response?: {}; @@ -7380,7 +7404,8 @@ declare namespace ts.server { * All projects that include this file */ readonly containingProjects: Project[]; - private formatCodeSettings; + private formatSettings; + private preferences; private textStorage; constructor(host: ServerHost, fileName: NormalizedPath, scriptKind: ScriptKind, hasMixedContent: boolean, path: Path); isScriptOpen(): boolean; @@ -7389,13 +7414,14 @@ declare namespace ts.server { getSnapshot(): IScriptSnapshot; private ensureRealPath; getFormatCodeSettings(): FormatCodeSettings; + getPreferences(): UserPreferences; attachToProject(project: Project): boolean; isAttached(project: Project): boolean; detachFromProject(project: Project): void; detachAllProjects(): void; getDefaultProject(): Project; registerFileUpdate(): void; - setFormatOptions(formatSettings: FormatCodeSettings): void; + setOptions(formatSettings: FormatCodeSettings, preferences: UserPreferences): void; getLatestVersion(): string; saveTo(fileName: string): void; reloadFromFile(tempFileName?: NormalizedPath): void; @@ -7773,6 +7799,7 @@ declare namespace ts.server { function convertScriptKindName(scriptKindName: protocol.ScriptKindName): ScriptKind.Unknown | ScriptKind.JS | ScriptKind.JSX | ScriptKind.TS | ScriptKind.TSX; interface HostConfiguration { formatCodeOptions: FormatCodeSettings; + preferences: UserPreferences; hostInfo: string; extraFileExtensions?: JsFileExtensionInfo[]; } @@ -7881,7 +7908,8 @@ declare namespace ts.server { */ private ensureProjectStructuresUptoDate; private updateProjectIfDirty; - getFormatCodeOptions(file?: NormalizedPath): FormatCodeSettings; + getFormatCodeOptions(file: NormalizedPath): FormatCodeSettings; + getPreferences(file: NormalizedPath): UserPreferences; private onSourceFileChanged; private handleDeletedFile; private onConfigChangedForConfiguredProject; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 1f64dd12ab8db..d7929bc2e60ec 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4324,6 +4324,12 @@ declare namespace ts { isKnownTypesPackageName?(name: string): boolean; installPackage?(options: InstallPackageOptions): Promise; } + interface UserPreferences { + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + } interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): Diagnostic[]; @@ -4341,7 +4347,7 @@ declare namespace ts { getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo; - getCompletionEntryDetails(fileName: string, position: number, name: string, options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails; + getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan; @@ -4371,8 +4377,8 @@ declare namespace ts { getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion; isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings): ReadonlyArray; - getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings): CombinedCodeActions; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray; + getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; applyCodeActionCommand(action: CodeActionCommand): Promise; applyCodeActionCommand(action: CodeActionCommand[]): Promise; applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; @@ -4382,9 +4388,9 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; - organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray; + getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; dispose(): void; @@ -4394,9 +4400,12 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - interface GetCompletionsAtPositionOptions { - includeExternalModuleExports: boolean; - includeInsertTextCompletions: boolean; + /** @deprecated Use UserPreferences */ + interface GetCompletionsAtPositionOptions extends UserPreferences { + /** @deprecated Use includeCompletionsForModuleExports */ + includeExternalModuleExports?: boolean; + /** @deprecated Use includeCompletionsWithInsertText */ + includeInsertTextCompletions?: boolean; } interface ApplyCodeActionCommandResult { successMessage: string; @@ -5065,7 +5074,7 @@ declare namespace ts { } declare namespace ts { /** The version of the language service API */ - const servicesVersion = "0.7"; + const servicesVersion = "0.8"; function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings; function displayPartsToString(displayParts: SymbolDisplayPart[]): string; function getDefaultCompilerOptions(): CompilerOptions; diff --git a/tests/cases/fourslash/codeFixClassImplementInterface_optionQuote.ts b/tests/cases/fourslash/codeFixClassImplementInterface_optionQuote.ts new file mode 100644 index 0000000000000..7fa6d8d9a9c97 --- /dev/null +++ b/tests/cases/fourslash/codeFixClassImplementInterface_optionQuote.ts @@ -0,0 +1,20 @@ +/// + +////interface I { +//// m(): void; +////} +////class C implements I {} + +verify.codeFix({ + description: "Implement interface 'I'", + newFileContent: +`interface I { + m(): void; +} +class C implements I { + m(): void { + throw new Error('Method not implemented.'); + } +}`, + preferences: { quotePreference: "single" } +}); diff --git a/tests/cases/fourslash/completionListInvalidMemberNames_escapeQuote.ts b/tests/cases/fourslash/completionListInvalidMemberNames_escapeQuote.ts index 8d6dde442a1be..e61c9bc874a02 100644 --- a/tests/cases/fourslash/completionListInvalidMemberNames_escapeQuote.ts +++ b/tests/cases/fourslash/completionListInvalidMemberNames_escapeQuote.ts @@ -1,7 +1,12 @@ /// -////declare const x: { '"': 0 }; +////declare const x: { "\"'": 0 }; ////x[|./**/|]; const replacementSpan = test.ranges()[0]; -verify.completionsAt("", [{ name: '"', insertText: '["\\""]', replacementSpan }], { includeInsertTextCompletions: true }); +verify.completionsAt("", [{ name: `"'`, insertText: `["\\"'"]`, replacementSpan }], { includeInsertTextCompletions: true }); + +verify.completionsAt("", [{ name: `"'`, insertText: `['"\\'']`, replacementSpan }], { + includeInsertTextCompletions: true, + quotePreference: "single", +}); diff --git a/tests/cases/fourslash/completionsImport_quoteStyle.ts b/tests/cases/fourslash/completionsImport_quoteStyle.ts new file mode 100644 index 0000000000000..cd3cc805daaa6 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_quoteStyle.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////export const foo = 0; + +// @Filename: /b.ts +////fo/**/ + +goTo.marker(""); +verify.applyCodeActionFromCompletion("", { + name: "foo", + source: "/a", + description: `Import 'foo' from module "./a"`, + preferences: { + quotePreference: "single", + }, + newFileContent: `import { foo } from './a'; + +fo`, +}); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index fd53a5acf2e8a..ed239d64f2e38 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -152,9 +152,7 @@ declare namespace FourSlashInterface { kind?: string | { kind?: string, kindModifiers?: string }, spanIndex?: number, hasAction?: boolean, - options?: { - includeExternalModuleExports?: boolean, - includeInsertTextCompletions?: boolean, + options?: UserPreferences & { sourceDisplay?: string, isRecommended?: true, insertText?: string, @@ -181,6 +179,7 @@ declare namespace FourSlashInterface { newRangeContent?: string, errorCode?: number, index?: number, + preferences?: UserPreferences, }); codeFixAvailable(options?: Array<{ description: string, actions?: Array<{ type: string, data: {} }>, commands?: {}[] }>): void; applicableRefactorAvailableAtMarker(markerName: string): void; @@ -197,10 +196,7 @@ declare namespace FourSlashInterface { class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; caretAtMarker(markerName?: string): void; - completionsAt(markerName: string, completions: ReadonlyArray, options?: { - isNewIdentifierLocation?: boolean; - includeInsertTextCompletions?: boolean; - }): void; + completionsAt(markerName: string, completions: ReadonlyArray, options?: CompletionsAtOptions): void; completionsAndDetailsAt( markerName: string, completions: { @@ -215,6 +211,7 @@ declare namespace FourSlashInterface { description: string, newFileContent?: string, newRangeContent?: string, + preferences?: UserPreferences, }); indentationIs(numberOfSpaces: number): void; indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle, baseIndentSize?: number): void; @@ -302,7 +299,7 @@ declare namespace FourSlashInterface { rangeIs(expectedText: string, includeWhiteSpace?: boolean): void; fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void; getAndApplyCodeFix(errorCode?: number, index?: number): void; - importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; + importFixAtPosition(expectedTextArray: string[], errorCode?: number, options?: UserPreferences): void; navigationBar(json: any, options?: { checkSpans?: boolean }): void; navigationTree(json: any, options?: { checkSpans?: boolean }): void; @@ -528,6 +525,15 @@ declare namespace FourSlashInterface { range?: Range; code: number; } + interface UserPreferences { + quotePreference?: "double" | "single"; + includeCompletionsForModuleExports?: boolean; + includeInsertTextCompletions?: boolean; + importModuleSpecifierPreference?: "relative" | "non-relative"; + } + interface CompletionsAtOptions extends UserPreferences { + isNewIdentifierLocation?: boolean; + } } declare function verifyOperationIsCancelled(f: any): void; declare var test: FourSlashInterface.test_; diff --git a/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl1.ts b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl1.ts index e690cfd3dbbf8..fa28dea5fe233 100644 --- a/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl1.ts +++ b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl1.ts @@ -23,3 +23,11 @@ f1();`, f1();` ]); + +verify.importFixAtPosition([ +`import { f1 } from "b/x"; + +f1();`, +], /*errorCode*/ undefined, { + importModuleSpecifierPreference: "non-relative", +}); diff --git a/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl2.ts b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl2.ts index 658bd25823feb..8fc9a97afe344 100644 --- a/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl2.ts +++ b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl2.ts @@ -23,3 +23,12 @@ f1();`, f1();` ]); + +verify.importFixAtPosition([ +`import { f1 } from "../b/x"; + +f1();`, +], /*errorCode*/ undefined, { + importModuleSpecifierPreference: "relative", +}); + diff --git a/tests/cases/fourslash/importNameCodeFix_quoteStyle.ts b/tests/cases/fourslash/importNameCodeFix_quoteStyle.ts new file mode 100644 index 0000000000000..b7479dea3e85c --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_quoteStyle.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /a.ts +////export const foo: number; + +// @Filename: /b.ts +////[|foo;|] + +goTo.file("/b.ts"); +verify.importFixAtPosition([ +`import { foo } from './a'; + +foo;`, +], /*errorCode*/ undefined, { + quotePreference: "single", +});