Skip to content

Commit

Permalink
Add expermiental support for sumneko/lua-language-server
Browse files Browse the repository at this point in the history
  • Loading branch information
josa42 committed Sep 28, 2020
1 parent c0105fc commit b442e3a
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .vim/coc-settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"coc.preferences.formatOnSaveFiletypes": ["javascript", "typescript", "json"]
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
},
"lua.version": {
"type": "string"
},
"lua.useSumnekoLs": {
"type": "boolean",
"default": false,
"description": "[EXPERIMENTAL] Use sumneko/lua-language-server as language server."
}
}
},
Expand Down Expand Up @@ -76,6 +81,7 @@
},
"dependencies": {
"@types/which": "^1.3.2",
"node-unzipper": "^0.0.3",
"tslib": "^2.0.1",
"which": "^2.0.2"
}
Expand Down
54 changes: 41 additions & 13 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
import {commands, ExtensionContext, LanguageClient, ServerOptions, workspace, services, LanguageClientOptions} from 'coc.nvim'
import {installLuaLsp, luaLspBin, commandExists} from './utils/tools'
import {version, updateLuaLsp} from './commands'
import {setStoragePath} from './utils/config'
import {
commands,
ExtensionContext,
LanguageClient,
ServerOptions,
workspace,
services,
LanguageClientOptions,
} from "coc.nvim"
import { installLuaLsp, luaLspBin, commandExists } from "./utils/tools"
import { version, updateLuaLsp } from "./commands"
import { setStoragePath } from "./utils/config"

interface LuaConfig {
enable: boolean
commandPath: string
}

export async function activate(context: ExtensionContext): Promise<void> {

setStoragePath(context.storagePath)

const config = workspace.getConfiguration().get('lua', {}) as LuaConfig
const config = workspace.getConfiguration().get("lua", {}) as LuaConfig
if (config.enable === false) {
return
}

const command = config.commandPath || await luaLspBin()
if (!await commandExists(command)) {
await installLuaLsp()
const [command, args] = config.commandPath ? [config.commandPath, []] : await luaLspBin()

if (!(await commandExists(command))) {
const useSumnekoLs = workspace.getConfiguration().get("lua.useSumnekoLs", false)

const name = useSumnekoLs ? "sumneko/lua-language-server" : "Alloyed/lua-lsp"
await showInstallStatus(name, async () => {
await installLuaLsp()
})
}

const serverOptions: ServerOptions = {command}
const serverOptions: ServerOptions = { command, args }

const clientOptions: LanguageClientOptions = {
documentSelector: ['lua']
documentSelector: ["lua"],
}

const client = new LanguageClient('lua', 'lua-lsp', serverOptions, clientOptions)
const client = new LanguageClient("lua", "lua-lsp", serverOptions, clientOptions)

context.subscriptions.push(
services.registLanguageClient(client),
commands.registerCommand("lua.version", () => version()),
commands.registerCommand("lua.update.lua-lsp", () => updateLuaLsp(client)),
commands.registerCommand("lua.update.lua-lsp", () => updateLuaLsp(client))
)
}

async function showInstallStatus(name: string, fn: () => Promise<void>) {
const statusItem = workspace.createStatusBarItem(90, { progress: true })

statusItem.text = `Installing '${name}'`
statusItem.show()

try {
await fn()

workspace.showMessage(`Installed '${name}'`)
} catch (err) {
workspace.showMessage(`Failed to install '${name}'`, "error")
}

statusItem.hide()
}
151 changes: 151 additions & 0 deletions src/utils/installer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// https://githb.com/neovim/nvim-lspconfi/blob/e38ff05afc3ad5d4fa8b24b4b0619429125582de/la/nvim_lsp/sumneko_lua.lua

import crypto from "crypto"
import fs from "fs"
import fsp from "fs/promises"
import https from "https"
import os from "os"
import path from "path"
import unzipper from "unzipper"
import { exec } from "child_process"

const ninjaVersion = "v1.9.0"
const osPlatform = os.platform()
const tmpBaseDir = os.tmpdir()

const { join } = path

export async function install(dir: string): Promise<void> {
const { path: tmpDir, dispose } = await mkTmpDir("coc-lua")
const { ninjaZip, buildFile } = osEnv()

const ninjaUrl = `https://github.com/ninja-build/ninja/releases/download/${ninjaVersion}/${ninjaZip}`
const llsUrl = "https://github.com/sumneko/lua-language-server.git"

const binDir = join(tmpDir, "bin")
const llsDir = join(tmpDir, "lua-ls")

const env = { ...process.env, PATH: `${process.env.PATH}:${binDir}` }

// Install ninja
await downloadZip(ninjaUrl, binDir)
await fsp.chmod(join(binDir, "ninja"), 0o755)

// clone
await sh(`git clone "${llsUrl}" "${llsDir}"`)
await sh("git submodule update --init --recursive", { cwd: llsDir })

// build
await sh(`ninja -f ${join("ninja", buildFile)}`, { cwd: join(llsDir, "3rd", "luamake"), env })
await sh(`${join(llsDir, "3rd", "luamake", "luamake")} rebuild`, { cwd: llsDir, env })

// copy files
for (const p of ["bin", "libs", "locale", "script", "main.lua", "platform.lua"]) {
await copy(join(llsDir, p), join(dir, p))
}

await dispose()
}

async function downloadZip(sourceUrl: string, targetPath: string) {
const dir = await mkTmpDir(sourceUrl)

const zipTmpPath = join(dir.path, "tmp.zip")

await download(sourceUrl, zipTmpPath)
await extractZip(zipTmpPath, targetPath)
await dir.dispose()
}

async function mkTmpDir(key: string): Promise<{ path: string; dispose: () => Promise<void> }> {
const hash = crypto.createHash("md5").update(key).digest("hex")
const dir = join(tmpBaseDir, hash)

await fsp.mkdir(dir, { recursive: true })

return { path: dir, dispose: async () => fsp.rmdir(dir, { recursive: true }) }
}

async function download(sourceUrl: string, targetPath: string) {
const file = fs.createWriteStream(targetPath)

return new Promise((resolve, reject) => {
const get = (url: string) =>
https.get(url, (res) => {
const { statusCode } = res

if (statusCode === 301 || statusCode === 302) {
return get(res.headers.location)
}

res
.on("data", (data) => file.write(data))
.on("end", () => (file.end(), resolve()))
.on("error", (err) => reject(err))
})

return get(sourceUrl)
})
}

export function osEnv(): { ninjaZip: string; buildFile: string; bin: string } {
switch (osPlatform) {
case "darwin":
return {
ninjaZip: "ninja-mac.zip",
buildFile: "macos.ninja",
bin: join("bin", "macOS", "lua-language-server"),
}
case "linux":
return {
ninjaZip: "ninja-linux.zip",
buildFile: "linux.ninja",
bin: join("bin", "macOS", "lua-language-server"),
}
case "win32":
return {
ninjaZip: "ninja-win.zip",
buildFile: "msvc.ninja",
bin: join("bin", "macOS", "lua-language-server.exe"),
}
}
return { ninjaZip: "", buildFile: "", bin: "" }
}

async function extractZip(zipPath: string, outputPath: string) {
return new Promise((resolve) => {
const extract = unzipper.Extract({ path: outputPath })
extract.on("close", resolve)

fs.createReadStream(zipPath).pipe(extract)
})
}

async function sh(cmd: string, options?: { cwd?: string; env?: { [key: string]: string } }): Promise<[string, string]> {
return new Promise((resolve, reject) => {
exec(cmd, options || {}, (err: Error, stdout: string, stderr: string) => {
if (err !== null) {
return reject(err)
}

resolve([stdout as string, stderr as string])
})
})
}

async function copy(src: string, dest: string) {
try {
const isDirectory = (await fsp.stat(src)).isDirectory()
if (isDirectory) {
await fsp.mkdir(dest, { recursive: true })

for (const file of await fsp.readdir(src)) {
await copy(join(src, file), join(dest, file))
}
} else {
await fsp.copyFile(src, dest)
}
} catch (err) {
// empty
}
}
53 changes: 33 additions & 20 deletions src/utils/tools.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
import path from 'path'
import fs from 'fs'
import {workspace} from 'coc.nvim'
import which from 'which'
import {configDir} from './config'
import path from "path"
import fs from "fs"
import { workspace } from "coc.nvim"
import which from "which"
import { configDir } from "./config"
import { osEnv, install } from "./installer"

const lspDir = "sumneko_lua"

export async function installLuaLsp(force = false): Promise<void> {
if (!force && await luaLspExists()) {
if (!force && (await luaLspExists())) {
return
}
const baseDir = await configDir('tools')

const useSumnekoLs = workspace.getConfiguration().get("lua.useSumnekoLs", false)
if (useSumnekoLs) {
return install(await configDir("tools", lspDir))
}

const baseDir = await configDir("tools")
let installCmd = `luarocks install --tree ${baseDir} --server=http://luarocks.org/dev lua-lsp`

const luaVersion = workspace.getConfiguration().get('lua', {})['version']
if(luaVersion) {
const luaVersion = workspace.getConfiguration().get("lua", {})["version"]
if (luaVersion) {
installCmd += ` --lua-version=${luaVersion}`
}

await workspace.runTerminalCommand(installCmd)
}

export async function luaLspBin(): Promise<string> {
let executable = 'lua-lsp'
if (process.platform === 'win32') {
// binary installed by luarocks under Windows has extension '.bat'
executable += '.bat'
export async function luaLspBin(): Promise<[string, string[]]> {
const baseDir = await configDir("tools")

const useSumnekoLs = workspace.getConfiguration().get("lua.useSumnekoLs", false)
if (useSumnekoLs) {
const { bin } = osEnv()
return [path.join(baseDir, lspDir, bin), ["-E", path.join(baseDir, lspDir, "main.lua")]]
}
return path.join(await configDir('tools', 'bin'), executable)

// binary installed by luarocks under Windows has extension '.bat'
const bin = process.platform === "win32" ? "lua-lsp.bat" : "lua-lsp"

return [path.join(baseDir, "bin", bin), []]
}

export async function luaLspExists(): Promise<boolean> {
const bin = await luaLspBin()
return new Promise(resolve => fs.open(bin, 'r', (err) => resolve(err === null)))
const [bin] = await luaLspBin()
return new Promise((resolve) => fs.open(bin, "r", (err) => resolve(err === null)))
}

export async function commandExists(command: string): Promise<boolean> {
return new Promise(resolve => which(command, (err) => resolve(err == null)))
return new Promise((resolve) => which(command, (err) => resolve(err == null)))
}


32 changes: 32 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ bser@2.1.1, bser@^2.1.1:
dependencies:
node-int64 "^0.4.0"

buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=

buffer-indexof-polyfill@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c"
Expand Down Expand Up @@ -811,6 +816,13 @@ fb-watchman@^2.0.1:
dependencies:
bser "2.1.1"

fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"

file-entry-cache@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
Expand Down Expand Up @@ -1291,6 +1303,13 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=

node-unzipper@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/node-unzipper/-/node-unzipper-0.0.3.tgz#92efb4cb39e2695f4dc3035004f46df976631649"
integrity sha512-e8yr0LP2Zex33mybsUmlVpOa0rZKwQmg3EERhM/dcu9I2bC+b/+P/Kj5QiztUkLCibn/CUL53CGD/aJts9nbVg==
dependencies:
yauzl "^2.10.0"

npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
Expand Down Expand Up @@ -1369,6 +1388,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==

pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=

picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
Expand Down Expand Up @@ -1893,3 +1917,11 @@ yargs@^16.0.3:
string-width "^4.2.0"
y18n "^5.0.1"
yargs-parser "^20.0.0"

yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"

0 comments on commit b442e3a

Please sign in to comment.