diff --git a/Readme.md b/Readme.md index 62edcbc..b622541 100644 --- a/Readme.md +++ b/Readme.md @@ -75,6 +75,7 @@ Almost the same as VSCode. - `tsserver.restart` - `tsserver.organizeImports` - `tsserver.watchBuild` + - `tsserver.findAllFileReferences` - Code completion support. - Go to definition (more info in [microsoft/TypeScript#37777](https://github.com/microsoft/TypeScript/issues/37777)) - Code validation. @@ -152,6 +153,11 @@ for guide of coc.nvim's configuration. default: `true` - `typescript.suggest.completeFunctionCalls`:Enable snippet for method suggestion, default: `true` +- `typescript.suggest.includeCompletionsForImportStatements`: Enable/disable + auto-import-style completions on partially-typed import statements. Requires using + TypeScript 4.3+ in the workspace, default: `true` +- `typescript.suggest.includeCompletionsWithSnippetText`: Enable snippet completions + from TS Server. Requires using TypeScript 4.3+ in the workspace, default: `true` - `typescript.format.enabled`:Enable/disable format of typescript files. - `typescript.format.insertSpaceAfterCommaDelimiter` default: `true` - `typescript.format.insertSpaceAfterConstructor` default: `false` @@ -200,6 +206,9 @@ for guide of coc.nvim's configuration. default: `true` - `javascript.suggest.completeFunctionCalls`:Enable snippet for method suggestion, default: `true` +- `javascript.suggest.includeCompletionsForImportStatements`: Enable/disable + auto-import-style completions on partially-typed import statements. Requires + using TypeScript 4.3+ in the workspace, default: `true` - `javascript.format.insertSpaceAfterCommaDelimiter` default: `true` - `javascript.format.insertSpaceAfterConstructor` default: `false` - `javascript.format.insertSpaceAfterSemicolonInForStatements` default: `true` diff --git a/package.json b/package.json index 9a84a59..a1824b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coc-tsserver", - "version": "1.7.0", + "version": "1.8.1", "description": "tsserver extension for coc.nvim", "main": "lib/index.js", "publisher": "chemzqm", @@ -89,6 +89,11 @@ "category": "TSServer", "command": "tsserver.restart" }, + { + "title": "Find File References", + "category": "TSServer", + "command": "tsserver.findAllFileReferences" + }, { "title": "Run `tsc --watch` for current project by use vim's job feature.", "category": "TSServer", @@ -109,6 +114,11 @@ "default": true, "description": "Enable tsserver extension" }, + "tsserver.tsconfigPath": { + "type": "string", + "default": "tsconfig.json", + "description": "Path to tsconfig file for the `tsserver.watchBuild` command. Defaults to `tsconfig.json`." + }, "tsserver.locale": { "type": [ "string", @@ -272,12 +282,19 @@ }, "typescript.preferences.importModuleSpecifier": { "type": "string", - "default": "auto", + "default": "shortest", "description": "Preferred path style for auto imports.", + "enumDescriptions": [ + "Prefers a non-relative import only if one is available that has fewer path segments than a relative import", + "Prefers a relative path to the imported file location", + "Prefers a non-relative import based on the `baseUrl` or `paths` configured in your `jsconfig.json` / `tsconfig.json`.", + "Prefers a non-relative import only if the relative import path would leave the package or project directory. Requires using TypeScript 4.2+ in the workspace." + ], "enum": [ - "non-relative", + "shortest", "relative", - "auto" + "non-relative", + "project-relative" ] }, "typescript.preferences.importModuleSpecifierEnding": { @@ -326,6 +343,10 @@ "default": true, "description": "Enable/disable suggest paths in import statement and require calls" }, + "typescript.suggest.importStatements": { + "type": "boolean", + "default": true + }, "typescript.suggest.autoImports": { "type": "boolean", "default": true, @@ -336,6 +357,18 @@ "default": true, "description": "Enable snippet for method suggestion" }, + "typescript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "Enable/disable auto-import-style completions on partially-typed import statements. Requires using TypeScript 4.3+ in the workspace.", + "scope": "resource" + }, + "typescript.suggest.includeCompletionsWithSnippetText": { + "type": "boolean", + "default": true, + "description": "Enable/disable snippet completions from TS Server. Requires using TypeScript 4.3+ in the workspace.", + "scope": "resource" + }, "typescript.format.enabled": { "type": "boolean", "default": true, @@ -454,12 +487,19 @@ }, "javascript.preferences.importModuleSpecifier": { "type": "string", - "default": "auto", + "default": "shortest", "description": "Preferred path style for auto imports.", + "enumDescriptions": [ + "Prefers a non-relative import only if one is available that has fewer path segments than a relative import", + "Prefers a relative path to the imported file location", + "Prefers a non-relative import based on the `baseUrl` or `paths` configured in your `jsconfig.json` / `tsconfig.json`.", + "Prefers a non-relative import only if the relative import path would leave the package or project directory. Requires using TypeScript 4.2+ in the workspace." + ], "enum": [ - "auto", + "shortest", + "relative", "non-relative", - "relative" + "project-relative" ] }, "javascript.preferences.importModuleSpecifierEnding": { @@ -522,6 +562,12 @@ "default": true, "description": "Enable snippet for method suggestion" }, + "javascript.suggest.includeCompletionsForImportStatements": { + "type": "boolean", + "default": true, + "description": "Enable/disable auto-import-style completions on partially-typed import statements. Requires using TypeScript 4.3+ in the workspace.", + "scope": "resource" + }, "javascript.format.enabled": { "type": "boolean", "default": true, @@ -630,12 +676,12 @@ "devDependencies": { "@types/node": "^10.12.0", "coc.nvim": "^0.0.80", + "esbuild": "^0.8.29", "semver": "^7.3.2", "vscode-languageserver-protocol": "^3.15.3", - "esbuild": "^0.8.29", "which": "^2.0.2" }, "dependencies": { - "typescript": "^4.1.3" + "typescript": "^4.3.2" } } diff --git a/src/index.ts b/src/index.ts index f3d4ea0..cdd0936 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { commands, ExtensionContext, services, workspace } from 'coc.nvim' import TsserverService from './server' -import { AutoFixCommand, Command, ConfigurePluginCommand, OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' +import { AutoFixCommand, Command, ConfigurePluginCommand, FileReferencesCommand, OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' import { OrganizeImportsCommand } from './server/organizeImports' import { PluginManager } from './utils/plugins' @@ -21,6 +21,7 @@ export async function activate(context: ExtensionContext): Promise { registCommand(new ConfigurePluginCommand(pluginManager)) registCommand(new AutoFixCommand(service)) registCommand(new ReloadProjectsCommand(service)) + registCommand(new FileReferencesCommand(service)) registCommand(new OpenTsServerLogCommand(service)) registCommand(new TypeScriptGoToProjectConfigCommand(service)) registCommand(new OrganizeImportsCommand(service)) diff --git a/src/server/commands.ts b/src/server/commands.ts index 701f27b..78dfef0 100644 --- a/src/server/commands.ts +++ b/src/server/commands.ts @@ -1,9 +1,10 @@ import { commands, diagnosticManager, CancellationToken, Diagnostic, Disposable, ServiceStat, Uri as URI, window, workspace } from 'coc.nvim' -import { Range, TextEdit } from 'vscode-languageserver-types' +import { Location, Position, Range, TextEdit } from 'vscode-languageserver-types' import TsserverService from '../server' import { PluginManager } from '../utils/plugins' import * as Proto from './protocol' import TypeScriptServiceClientHost from './typescriptServiceClientHost' +import API from './utils/api' import { nodeModules } from './utils/helper' import { installModules } from './utils/modules' import * as typeConverters from './utils/typeConverters' @@ -184,6 +185,39 @@ export class ConfigurePluginCommand implements Command { } } +export class FileReferencesCommand implements Command { + public readonly id = 'tsserver.findAllFileReferences' + public static readonly minVersion = API.v420 + + public constructor( + private readonly service: TsserverService + ) {} + + public async execute() { + const client = await this.service.getClientHost() + if (client.serviceClient.apiVersion.lt(FileReferencesCommand.minVersion)) { + window.showMessage('Find file references failed. Requires TypeScript 4.2+.', 'error') + return + } + + const doc = await workspace.document + let { languageId } = doc.textDocument + if (client.serviceClient.modeIds.indexOf(languageId) == -1) return + + const openedFiledPath = client.serviceClient.toOpenedFilePath(doc.uri) + if (!openedFiledPath) return + + const response = await client.serviceClient.execute('fileReferences', { file: openedFiledPath }, CancellationToken.None) + if (response.type !== 'response' || !response.body) return + + const locations: Location[] = (response as Proto.FileReferencesResponse).body.refs.map(r => + typeConverters.Location.fromTextSpan(client.serviceClient.toResource(r.file), r) + ) + + await commands.executeCommand('editor.action.showReferences', doc.uri, Position.create(0, 0), locations) + } +} + export function registCommand(cmd: Command): Disposable { let { id, execute } = cmd return commands.registerCommand(id as string, execute, cmd) diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index 3245a77..49060fd 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -49,7 +49,7 @@ class ApplyCompletionCodeActionCommand implements CommandItem { export default class TypeScriptCompletionItemProvider implements CompletionItemProvider { - public static readonly triggerCharacters = ['.', '"', '\'', '`', '/', '@', '<', '#'] + public static readonly triggerCharacters = ['.', '"', '\'', '`', '/', '@', '<', '#', ' '] private completeOption: SuggestOptions constructor( @@ -148,7 +148,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP dotAccessorContext = { range, text } } } - isIncomplete = (response as any).metadata && (response as any).metadata.isIncomplete + isIncomplete = !!response.body.isIncomplete || (response as any).metadata && (response as any).metadata.isIncomplete entries = response.body.entries } catch (e) { if (e.message == 'No content available.') { @@ -194,6 +194,8 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP case '#': // Workaround for https://github.com/microsoft/TypeScript/issues/36367 return this.client.apiVersion.lt(API.v381) ? undefined : '#' + case ' ': + return this.client.apiVersion.gte(API.v430) ? ' ' : undefined case '.': case '"': @@ -220,7 +222,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP ): Promise { if (item == null) return undefined - let { uri, position, source, name } = item.data + let { uri, position, source, name, data } = item.data const filepath = this.client.toPath(uri) if (!filepath) return undefined let document = workspace.getDocument(uri) @@ -230,7 +232,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP filepath, position ), - entryNames: [source ? { name, source } : name] + entryNames: [source ? { name, source, data } : name] } let response: ServerResponse.Response @@ -338,6 +340,13 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } } + if (triggerCharacter === ' ') { + if (!this.completeOption.importStatementSuggestions || !this.client.apiVersion.lt(API.v430)) { + return false + } + return pre === 'import '; + } + return true } diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts index 5ec9c72..d465cff 100644 --- a/src/server/features/fileConfigurationManager.ts +++ b/src/server/features/fileConfigurationManager.ts @@ -37,6 +37,9 @@ export interface SuggestOptions { readonly completeFunctionCalls: boolean readonly autoImports: boolean readonly includeAutomaticOptionalChainCompletions: boolean + readonly importStatementSuggestions: boolean + readonly includeCompletionsForImportStatements: boolean + readonly includeCompletionsWithSnippetText: boolean } export default class FileConfigurationManager { @@ -156,6 +159,9 @@ export default class FileConfigurationManager { paths: config.get('paths', true), completeFunctionCalls: config.get('completeFunctionCalls', true), autoImports: config.get('autoImports', true), + importStatementSuggestions: config.get('importStatements', true), + includeCompletionsForImportStatements: config.get('includeCompletionsForImportStatements', true), + includeCompletionsWithSnippetText: config.get('includeCompletionsWithSnippetText', true), includeAutomaticOptionalChainCompletions: config.get('includeAutomaticOptionalChainCompletions', true) } } @@ -173,6 +179,8 @@ export default class FileConfigurationManager { allowTextChangesInNewFiles: uri.startsWith('file:'), allowRenameOfImportPath: true, providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true) === false ? false : config.get('useAliasesForRenames', true), + includeCompletionsForImportStatements: this.getCompleteOptions(language).includeCompletionsForImportStatements, + includeCompletionsWithSnippetText: this.getCompleteOptions(language).includeCompletionsWithSnippetText, } return preferences } @@ -188,17 +196,19 @@ export default class FileConfigurationManager { } } -type ModuleImportType = 'relative' | 'non-relative' | 'auto' +type ModuleImportType = 'shortest' | 'project-relative' | 'relative' | 'non-relative' -function getImportModuleSpecifier(config): ModuleImportType { +function getImportModuleSpecifier(config: WorkspaceConfiguration): ModuleImportType { let val = config.get('importModuleSpecifier') switch (val) { + case 'project-relative': + return 'project-relative' case 'relative': return 'relative' case 'non-relative': return 'non-relative' default: - return 'auto' + return undefined } } diff --git a/src/server/features/watchBuild.ts b/src/server/features/watchBuild.ts index 283eb2e..ea40c7b 100644 --- a/src/server/features/watchBuild.ts +++ b/src/server/features/watchBuild.ts @@ -32,6 +32,7 @@ export default class WatchProject implements Disposable { ) { this.statusItem = window.createStatusBarItem(1, { progress: true }) let task = this.task = workspace.createTask('TSC') + this.disposables.push(commands.registerCommand(WatchProject.id, async () => { let opts = this.options = await this.getOptions() await this.start(opts) @@ -117,15 +118,18 @@ export default class WatchProject implements Disposable { window.showMessage(`Local & global tsc not found`, 'error') return } - let find = await workspace.findUp(['tsconfig.json']) + + const tsconfigPath = workspace.getConfiguration('tsserver').get('tsconfigPath', 'tsconfig.json'); + let find = await workspace.findUp([tsconfigPath]) if (!find) { - window.showMessage('tsconfig.json not found!', 'error') + window.showMessage(`${tsconfigPath} not found!`, 'error') return } + let root = path.dirname(find) return { cmd: tscPath, - args: ['-p', 'tsconfig.json', '--watch', 'true', '--pretty', 'false'], + args: ['-p', tsconfigPath, '--watch', 'true', '--pretty', 'false'], cwd: root } } diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index 1dc60eb..47af526 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -19,7 +19,7 @@ export namespace ServerResponse { constructor( public readonly reason: string - ) { } + ) {} } // tslint:disable-next-line: new-parens @@ -80,6 +80,7 @@ export interface TypeScriptRequestTypes { 'selectionRange': [Proto.SelectionRangeRequestArgs, Proto.SelectionRangeResponse] 'signatureHelp': [Proto.SignatureHelpRequestArgs, Proto.SignatureHelpResponse] 'typeDefinition': [Proto.FileLocationRequestArgs, Proto.TypeDefinitionResponse] + 'fileReferences': [Proto.FileRequestArgs, Proto.FileReferencesResponse] } export interface ITypeScriptServiceClient { diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index c3bb9ba..6f6008a 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -39,7 +39,9 @@ export default class API { public static readonly v381 = API.fromSimpleString('3.8.1') public static readonly v390 = API.fromSimpleString('3.9.0') public static readonly v400 = API.fromSimpleString('4.0.0') - public static readonly v401 = API.fromSimpleString('4.0.1'); + public static readonly v401 = API.fromSimpleString('4.0.1') + public static readonly v420 = API.fromSimpleString('4.2.0') + public static readonly v430 = API.fromSimpleString('4.3.0') public static fromVersionString(versionString: string): API { let version = semver.valid(versionString) diff --git a/src/server/utils/completionItem.ts b/src/server/utils/completionItem.ts index f12c142..d6b4cbb 100644 --- a/src/server/utils/completionItem.ts +++ b/src/server/utils/completionItem.ts @@ -37,7 +37,7 @@ export function convertCompletionEntry( if (tsEntry.isRecommended) { preselect = true } - if (tsEntry.source) { + if (tsEntry.source && tsEntry.hasAction) { // De-prioritze auto-imports https://github.com/Microsoft/vscode/issues/40311 sortText = '\uffff' + sortText } else { @@ -53,13 +53,18 @@ export function convertCompletionEntry( let insertText = tsEntry.insertText let commitCharacters = getCommitCharacters(tsEntry, context) + if (tsEntry.isImportStatementCompletion) { + insertText = label + insertTextFormat = InsertTextFormat.Snippet + } + let textEdit: TextEdit | null = null if (tsEntry.replacementSpan) { let { start, end } = tsEntry.replacementSpan if (start.line == end.line) { textEdit = { range: Range.create(start.line - 1, start.offset - 1, end.line - 1, end.offset - 1), - newText: insertText || label + newText: tsEntry.insertText || label } } } @@ -101,6 +106,7 @@ export function convertCompletionEntry( uri, position, name: tsEntry.name, + data: tsEntry.data, source: tsEntry.source || '' } } diff --git a/yarn.lock b/yarn.lock index 75e347c..ed3d2de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,10 +27,10 @@ semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -typescript@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" - integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== +typescript@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== vscode-jsonrpc@^5.0.1: version "5.0.1"