add install modules codeAction
This commit is contained in:
parent
93f82a96a6
commit
dab8e1509d
6 changed files with 196 additions and 16 deletions
|
@ -421,7 +421,7 @@
|
|||
"@types/fast-diff": "^1.2.0",
|
||||
"@types/find-up": "^2.1.1",
|
||||
"@types/node": "^10.12.18",
|
||||
"coc.nvim": "0.0.39",
|
||||
"coc.nvim": "0.0.52",
|
||||
"rimraf": "^2.6.2",
|
||||
"tslint": "^5.12.0"
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as Proto from './protocol'
|
|||
import TypeScriptServiceClientHost from './typescriptServiceClientHost'
|
||||
import * as typeConverters from './utils/typeConverters'
|
||||
import { TextEdit, Range } from 'vscode-languageserver-types'
|
||||
import { installModules } from './utils/modules'
|
||||
|
||||
const nodeModules = [
|
||||
'assert',
|
||||
|
@ -130,11 +131,21 @@ export class AutoFixCommand implements Command {
|
|||
}
|
||||
let file = this.client.serviceClient.toPath(document.uri)
|
||||
let diagnostics = diagnosticManager.getDiagnostics(document.uri)
|
||||
diagnostics = diagnostics.filter(x => autoFixableDiagnosticCodes.has(x.code as number))
|
||||
if (diagnostics.length == 0) {
|
||||
workspace.showMessage('No autofixable diagnostics found', 'warning')
|
||||
return
|
||||
let missingDiagnostics = diagnostics.filter(o => o.code == 2307)
|
||||
if (missingDiagnostics.length) {
|
||||
let names = missingDiagnostics.map(o => {
|
||||
let ms = o.message.match(/module\s'(.+)'\./)
|
||||
return ms ? ms[1] : null
|
||||
})
|
||||
names = names.filter(s => s != null)
|
||||
if (names.length) {
|
||||
installModules(document.uri, names).catch(e => {
|
||||
console.error(e.message) // tslint:disable-line
|
||||
})
|
||||
}
|
||||
}
|
||||
diagnostics = diagnostics.filter(x => autoFixableDiagnosticCodes.has(x.code as number))
|
||||
if (diagnostics.length == 0) return
|
||||
diagnostics = diagnostics.reduce((arr, curr) => {
|
||||
if (curr.code == 2304 && arr.findIndex(o => o.message == curr.message) != -1) return arr
|
||||
arr.push(curr)
|
||||
|
|
56
src/server/features/moduleInstall.ts
Normal file
56
src/server/features/moduleInstall.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { commands } from 'coc.nvim'
|
||||
import { Command } from 'coc.nvim/lib/commands'
|
||||
import { CodeActionProvider } from 'coc.nvim/lib/provider'
|
||||
import { CancellationToken, CodeAction, CodeActionContext, Range, TextDocument } from 'vscode-languageserver-protocol'
|
||||
import Uri from 'vscode-uri'
|
||||
import { ITypeScriptServiceClient } from '../typescriptService'
|
||||
import { installModules } from '../utils/modules'
|
||||
|
||||
class InstallModuleCommand implements Command {
|
||||
public static readonly ID = '_tsserver.installModule'
|
||||
public readonly id = InstallModuleCommand.ID
|
||||
|
||||
public async execute(
|
||||
uri: string,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
await installModules(uri, [name])
|
||||
}
|
||||
}
|
||||
|
||||
export default class InstallModuleProvider implements CodeActionProvider {
|
||||
|
||||
constructor(private readonly client: ITypeScriptServiceClient) {
|
||||
commands.register(new InstallModuleCommand(), true)
|
||||
}
|
||||
|
||||
public async provideCodeActions(
|
||||
document: TextDocument,
|
||||
_range: Range,
|
||||
context: CodeActionContext,
|
||||
_token: CancellationToken
|
||||
): Promise<CodeAction[] | null> {
|
||||
const uri = Uri.parse(document.uri)
|
||||
if (uri.scheme != 'file') return null
|
||||
let { diagnostics } = context
|
||||
let diags = diagnostics.filter(s => s.code == 2307)
|
||||
let names = diags.map(o => {
|
||||
let ms = o.message.match(/module\s'(.+)'\./)
|
||||
return ms ? ms[1] : null
|
||||
})
|
||||
names = names.filter(s => s != null)
|
||||
if (!names.length) return null
|
||||
let actions: CodeAction[] = []
|
||||
for (let name of names) {
|
||||
let title = `install ${name}`
|
||||
let command = {
|
||||
title: `install ${name}`,
|
||||
command: InstallModuleCommand.ID,
|
||||
arguments: [document.uri, name]
|
||||
}
|
||||
let codeAction = CodeAction.create(title, command)
|
||||
actions.push(codeAction)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename'
|
|||
import WatchBuild from './features/watchBuild'
|
||||
import WorkspaceSymbolProvider from './features/workspaceSymbols'
|
||||
import TypeScriptServiceClient from './typescriptServiceClient'
|
||||
import InstallModuleProvider from './features/moduleInstall'
|
||||
import API from './utils/api'
|
||||
import { LanguageDescription } from './utils/languageDescription'
|
||||
import TypingsStatus from './utils/typingsStatus'
|
||||
|
@ -238,6 +239,13 @@ export default class LanguageProvider {
|
|||
[CodeActionKind.Refactor]))
|
||||
}
|
||||
|
||||
this.disposables.push(
|
||||
languages.registerCodeActionProvider(
|
||||
languageIds,
|
||||
new InstallModuleProvider(client),
|
||||
'tsserver')
|
||||
)
|
||||
|
||||
this.disposables.push(
|
||||
languages.registerCodeActionProvider(
|
||||
languageIds,
|
||||
|
|
|
@ -33,25 +33,22 @@ export default class TypeScriptServiceClientHost implements Disposable {
|
|||
private reportStyleCheckAsWarnings = true
|
||||
|
||||
constructor(descriptions: LanguageDescription[]) {
|
||||
let timer: NodeJS.Timer
|
||||
const handleProjectChange = () => {
|
||||
setTimeout(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
this.triggerAllDiagnostics()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const configFileWatcher = workspace.createFileSystemWatcher('**/[tj]sconfig.json')
|
||||
this.disposables.push(configFileWatcher)
|
||||
configFileWatcher.onDidCreate(
|
||||
this.reloadProjects,
|
||||
this,
|
||||
this.disposables
|
||||
)
|
||||
configFileWatcher.onDidDelete(
|
||||
this.reloadProjects,
|
||||
this,
|
||||
this.disposables
|
||||
)
|
||||
configFileWatcher.onDidCreate(this.reloadProjects, this, this.disposables)
|
||||
configFileWatcher.onDidDelete(this.reloadProjects, this, this.disposables)
|
||||
configFileWatcher.onDidChange(handleProjectChange, this, this.disposables)
|
||||
const packageFileWatcher = workspace.createFileSystemWatcher('**/package.json')
|
||||
packageFileWatcher.onDidCreate(this.reloadProjects, this, this.disposables)
|
||||
packageFileWatcher.onDidChange(handleProjectChange, this, this.disposables)
|
||||
|
||||
this.client = new TypeScriptServiceClient()
|
||||
this.disposables.push(this.client)
|
||||
|
|
108
src/server/utils/modules.ts
Normal file
108
src/server/utils/modules.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { exec } from 'child_process'
|
||||
import path from 'path'
|
||||
import { workspace } from 'coc.nvim'
|
||||
import findUp from 'find-up'
|
||||
import Uri from 'vscode-uri'
|
||||
|
||||
export function runCommand(cmd: string, cwd: string, timeout?: number): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let timer: NodeJS.Timer
|
||||
if (timeout) {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`timeout after ${timeout}s`))
|
||||
}, timeout * 1000)
|
||||
}
|
||||
exec(cmd, { cwd }, (err, stdout) => {
|
||||
if (timer) clearTimeout(timer)
|
||||
if (err) {
|
||||
reject(new Error(`exited with ${err.code}`))
|
||||
return
|
||||
}
|
||||
resolve(stdout)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getManager(uri: string): string {
|
||||
let dir = path.dirname(Uri.parse(uri).fsPath)
|
||||
let res = findUp.sync(['yarn.lock', 'package-lock.json'], { cwd: dir })
|
||||
if (!res) return 'yarn'
|
||||
return res.endsWith('yarn.lock') ? 'yarn' : 'npm'
|
||||
}
|
||||
|
||||
function getRoot(uri: string): string {
|
||||
let dir = path.dirname(Uri.parse(uri).fsPath)
|
||||
let res = findUp.sync(['package.json'], { cwd: dir })
|
||||
if (!res) return dir
|
||||
return path.dirname(res)
|
||||
}
|
||||
|
||||
export async function moduleExists(name: string): Promise<boolean> {
|
||||
try {
|
||||
let content = await runCommand(`npm info ${name} --json`, process.cwd())
|
||||
if (!content) return false
|
||||
let obj = JSON.parse(content)
|
||||
if (obj.error != null) return false
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicates from the given array. The optional keyFn allows to specify
|
||||
* how elements are checked for equalness by returning a unique string for each.
|
||||
*/
|
||||
export function distinct<T>(array: T[], keyFn?: (t: T) => string): T[] {
|
||||
if (!keyFn) {
|
||||
return array.filter((element, position) => {
|
||||
return array.indexOf(element) === position
|
||||
})
|
||||
}
|
||||
|
||||
const seen: { [key: string]: boolean } = Object.create(null)
|
||||
return array.filter(elem => {
|
||||
const key = keyFn(elem)
|
||||
if (seen[key]) {
|
||||
return false
|
||||
}
|
||||
|
||||
seen[key] = true
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export async function installModules(uri: string, names: string[]): Promise<void> {
|
||||
names = distinct(names)
|
||||
let root = getRoot(uri)
|
||||
let arr = names.concat(names.map(s => `@types/${s}`))
|
||||
let statusItem = workspace.createStatusBarItem(99, { progress: true })
|
||||
statusItem.text = `Checking module ${arr.join(' ')}`
|
||||
statusItem.show()
|
||||
let exists = await Promise.all(arr.map(name => {
|
||||
return moduleExists(name).then(exists => {
|
||||
return exists ? name : null
|
||||
})
|
||||
}))
|
||||
let manager = getManager(uri)
|
||||
exists = exists.filter(s => s != null)
|
||||
if (!exists.length) return
|
||||
let devs = exists.filter(s => s.startsWith('@types'))
|
||||
let deps = exists.filter(s => devs.indexOf(s) == -1)
|
||||
statusItem.text = `Installing ${exists.join(' ')}`
|
||||
try {
|
||||
await Promise.all([deps, devs].map((names, i) => {
|
||||
let cmd = manager == 'npm' ? `npm i ${names.join(' ')}` : `yarn add ${names.join(' ')} --ignore-scripts --no-default-rc`
|
||||
if (i == 1) cmd = cmd + ' --dev'
|
||||
return runCommand(cmd, root)
|
||||
}))
|
||||
} catch (e) {
|
||||
statusItem.dispose()
|
||||
workspace.showMessage(`Install error ${e.message}`, 'error')
|
||||
return
|
||||
}
|
||||
statusItem.dispose()
|
||||
workspace.showMessage(`Installed: ${exists.join(' ')}`, 'more')
|
||||
}
|
Loading…
Add table
Reference in a new issue