add install modules codeAction

This commit is contained in:
chemzqm 2019-02-06 17:12:10 +08:00
parent 93f82a96a6
commit dab8e1509d
6 changed files with 196 additions and 16 deletions

View file

@ -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"
},

View file

@ -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)

View 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
}
}

View file

@ -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,

View file

@ -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
View 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')
}