diff --git a/.vscode/settings.json b/.vscode/settings.json index f75c884..bb88aff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "typescript.tsc.autoDetect": "off", "typescript.preferences.quoteStyle": "single", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", diff --git a/client/src/extension.ts b/client/src/extension.ts index ffcc692..94546fc 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -1,6 +1,5 @@ import * as path from "path"; import { - workspace, ExtensionContext, window, commands, @@ -27,36 +26,17 @@ let client: LanguageClient; //拡張機能を立ち上げたときに呼び出す関数 export function activate(context: ExtensionContext) { - // The server is implemented in node const serverModule = context.asAbsolutePath( path.join("server", "out", "server.js"), ); - // The debug options for the server - // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging - const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { - module: serverModule, - transport: TransportKind.ipc, - options: debugOptions, - }, + run: { module: serverModule, transport: TransportKind.stdio }, + debug: { module: serverModule, transport: TransportKind.stdio }, }; - // 対象とする言語。今回はplaintext const clientOptions: LanguageClientOptions = { - // Register the server for plain text documents - documentSelector: [ - { pattern: "**", scheme: "file" }, - { pattern: "**", scheme: "untitled" }, - ], - synchronize: { - // Notify the server about file changes to '.clientrc files contained in the workspace - fileEvents: workspace.createFileSystemWatcher("**/.clientrc"), - }, + documentSelector: [{ scheme: "file", language: "sql" }], }; // Create the language client and start the client. diff --git a/server/package-lock.json b/server/package-lock.json index 4c6bd84..97e916c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,10 +9,7 @@ "version": "1.0.0", "license": "BUSL-1.1", "dependencies": { - "uroborosql-fmt-napi": "file:uroborosql-fmt-napi-1.0.1.tgz", - "vscode-languageserver": "^9.0.0", - "vscode-languageserver-textdocument": "^1.0.4", - "vscode-uri": "^3.0.7" + "uroborosql-fmt-napi": "file:uroborosql-fmt-napi-1.0.1.tgz" }, "engines": { "node": "*" @@ -26,49 +23,6 @@ "engines": { "node": ">= 10" } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", - "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" - }, - "node_modules/vscode-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", - "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" } } } diff --git a/server/package.json b/server/package.json index 950c812..76b166b 100644 --- a/server/package.json +++ b/server/package.json @@ -10,10 +10,7 @@ }, "repository": {}, "dependencies": { - "uroborosql-fmt-napi": "file:uroborosql-fmt-napi-1.0.1.tgz", - "vscode-languageserver": "^9.0.0", - "vscode-languageserver-textdocument": "^1.0.4", - "vscode-uri": "^3.0.7" + "uroborosql-fmt-napi": "file:uroborosql-fmt-napi-1.0.1.tgz" }, "scripts": {} } diff --git a/server/src/server.ts b/server/src/server.ts index 324ea72..e420f41 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,354 +1,5 @@ -import { - createConnection, - TextDocuments, - ProposedFeatures, - InitializeParams, - DidChangeConfigurationNotification, - TextDocumentSyncKind, - InitializeResult, - TextEdit, - TextDocumentEdit, - Position, - Range, - DocumentFormattingRequest, - DocumentFilter, - DocumentFormattingRegistrationOptions, -} from "vscode-languageserver/node"; +import { runLanguageServer } from "uroborosql-fmt-napi"; -import { TextDocument } from "vscode-languageserver-textdocument"; - -import { runfmtWithSettings } from "uroborosql-fmt-napi"; -import * as fs from "fs"; - -import { performance } from "perf_hooks"; -import path = require("path"); -import { URI } from "vscode-uri"; -import { objectToSnake } from "ts-case-convert"; -// Create a connection for the server, using Node's IPC as a transport. -// Also include all preview / proposed LSP features. -const connection = createConnection(ProposedFeatures.all); - -// Create a simple text document manager. -const documents: TextDocuments = new TextDocuments(TextDocument); - -let hasConfigurationCapability = false; -let hasWorkspaceFolderCapability = false; -// let hasDiagnosticRelatedInformationCapability = false; - -connection.onInitialize((params: InitializeParams) => { - const capabilities = params.capabilities; - - // Does the client support the `workspace/configuration` request? - // If not, we fall back using global settings. - hasConfigurationCapability = !!( - capabilities.workspace && !!capabilities.workspace.configuration - ); - hasWorkspaceFolderCapability = !!( - capabilities.workspace && !!capabilities.workspace.workspaceFolders - ); - // hasDiagnosticRelatedInformationCapability = !!( - // capabilities.textDocument && - // capabilities.textDocument.publishDiagnostics && - // capabilities.textDocument.publishDiagnostics.relatedInformation - // ); - - const result: InitializeResult = { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, - }, - }; - if (hasWorkspaceFolderCapability) { - result.capabilities.workspace = { - workspaceFolders: { - supported: true, - }, - }; - } - return result; -}); - -connection.onInitialized(() => { - if (hasConfigurationCapability) { - // Register for all configuration changes. - connection.client.register( - DidChangeConfigurationNotification.type, - undefined, - ); - } - if (hasWorkspaceFolderCapability) { - connection.workspace.onDidChangeWorkspaceFolders(() => { - connection.console.log("Workspace folder change event received."); - }); - } - - const filter: DocumentFilter = { language: "sql" }; - const options: DocumentFormattingRegistrationOptions = { - documentSelector: [filter], - }; - connection.client.register(DocumentFormattingRequest.type, options); -}); - -type ConfigurationSettings = { - configurationFilePath: string; - debug: boolean | null | undefined; - tabSize: number | null | undefined; - complementAlias: boolean | null | undefined; - trimBindParam: boolean | null | undefined; - keywordCase: string | null | undefined; - identifierCase: string | null | undefined; - maxCharPerLine: number | null | undefined; - complementOuterKeyword: boolean | null | undefined; - complementColumnAsKeyword: boolean | null | undefined; - removeTableAsKeyword: boolean | null | undefined; - removeRedundantNest: boolean | null | undefined; - complementSqlId: boolean | null | undefined; - convertDoubleColonCast: boolean | null | undefined; - unifyNotEqual: boolean | null | undefined; - indentTab: boolean | null | undefined; - useParserErrorRecovery: boolean | null | undefined; -}; - -function getSettings(resource: string): Thenable { - return connection.workspace.getConfiguration({ - scopeUri: resource, - section: "uroborosql-fmt", - }); -} - -async function getWorkspaceFolder( - document: TextDocument, -): Promise { - const { scheme, fsPath } = URI.parse(document.uri); - - if (scheme === "untitled") { - const uri = (await connection.workspace.getWorkspaceFolders())?.[0]?.uri; - return uri ? URI.parse(uri).fsPath : undefined; - } - - if (fsPath) { - const workspaceFolders = await connection.workspace.getWorkspaceFolders(); - - if (workspaceFolders) { - const wsFolder = workspaceFolders.find((wsFolder) => { - const { scheme: wsScheme, fsPath: wsFsPath } = URI.parse(wsFolder.uri); - const relative = path.relative(wsFsPath, fsPath); - return ( - scheme == wsScheme && - relative && - !relative.startsWith("..") && - !path.isAbsolute(relative) - ); - }); - - return wsFolder ? wsFolder.uri : undefined; - } - } - return undefined; -} - -function getVSCodeOptions( - settings: ConfigurationSettings, - workspaceFolder: string | undefined, -): Partial | null { - if (!workspaceFolder) { - return null; - } - - // remove configurationFilePath - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { configurationFilePath, ...restConfiguration } = settings; - - // translate null (that means unsupecified option) to undefined - const removedNullSettings = Object.fromEntries( - Object.entries(restConfiguration).filter(([, value]) => value != null), - ); - - // to snake case for uroborosql-fmt - return objectToSnake(removedNullSettings); -} - -function determineConfigPath( - settings: ConfigurationSettings, - workspaceFolder: string | undefined, -): string | null { - if (!workspaceFolder) { - return null; - } - - // remove scheme - const workspaceFolderPath = URI.parse(workspaceFolder).fsPath; - - if (!settings.configurationFilePath) { - const defaultConfigPath = path.join( - workspaceFolderPath, - ".uroborosqlfmtrc.json", - ); - - // The path of configuration file is not specified. - // If defaultConfigPath doesn't exist, fomatters default config will be used. - if (!fs.existsSync(defaultConfigPath)) { - return null; - } - - return defaultConfigPath; - } - - let specifiedConfigPath = settings.configurationFilePath; - if (!path.isAbsolute(specifiedConfigPath)) { - specifiedConfigPath = path.join(workspaceFolderPath, specifiedConfigPath); - } - - if (!fs.existsSync(specifiedConfigPath)) { - // If the path is explicitly specified but the file does not exist, - // it is not formatted and an error is generated. - throw new Error(`${specifiedConfigPath} doesn't exist.`); - } - - return specifiedConfigPath; -} - -async function formatText( - uri: string, - textDocument: TextDocument, - version: number, - selections: Range[], -): Promise { - const workspaceFolder: string | undefined = - await getWorkspaceFolder(textDocument); - - const settings: ConfigurationSettings = await getSettings(uri); - - let configPath: string | null; - - try { - configPath = determineConfigPath(settings, workspaceFolder); - } catch (e) { - if (e instanceof Error) { - connection.window.showErrorMessage(e.message); - } - - return []; - } - - // version check - if (version !== textDocument.version) { - return []; - } - - // settings specified by vscode ui - const specifiedSettings = getVSCodeOptions(settings, workspaceFolder); - const settingsString = specifiedSettings - ? JSON.stringify(specifiedSettings, null, 2) - : "{}"; - const changes: TextEdit[] = []; - - console.log("VSCode settings:", settingsString); - console.log("settings path:", configPath?.toString()); - - // 全ての選択範囲に対して実行 - for (const selection of selections) { - // テキストを取得 - const text = textDocument.getText(selection); - if (!text.length) { - continue; - } - - let formattedText: string; - - try { - formattedText = runfmtWithSettings(text, settingsString, configPath); - // ステータスバーの背景を通常色に変更 - connection.sendRequest("custom/normal", []); - } catch (e) { - console.error(e); - // ステータスバーの背景を赤色に変更 - connection.sendRequest("custom/error", []); - return []; - } - - // フォーマット - changes.push(TextEdit.replace(selection, formattedText)); - } - - if (!changes.length) { - // テキスト全体を取得 - const text = textDocument.getText(); - - let formattedText: string; - const startTime = performance.now(); - try { - formattedText = runfmtWithSettings(text, settingsString, configPath); - // ステータスバーの背景を通常色に変更 - connection.sendRequest("custom/normal", []); - } catch (e) { - console.error(e); - // ステータスバーの背景を赤色に変更 - connection.sendRequest("custom/error", []); - return []; - } - //タイマーストップ - const endTime = performance.now(); - console.log("format complete: " + (endTime - startTime) + "ms"); // 何ミリ秒かかったかを表示する - - // フォーマット - changes.push( - TextEdit.replace( - Range.create( - Position.create(0, 0), - textDocument.positionAt(text.length), - ), - formattedText, - ), - ); - } - - return changes; -} - -// コマンド実行時に行う処理 -connection.onExecuteCommand(async (params) => { - if ( - params.command !== "uroborosql-fmt.executeFormat" || - params.arguments == null - ) { - return; - } - const uri = params.arguments[0].external; - // uriからドキュメントを取得 - const textDocument = documents.get(uri); - if (textDocument == null) { - return; - } - - const version = params.arguments[1]; - const selections = params.arguments[2]; - - const changes = await formatText(uri, textDocument, version, selections); - - // 変更を適用 - connection.workspace.applyEdit({ - documentChanges: [ - TextDocumentEdit.create( - { uri: textDocument.uri, version: textDocument.version }, - changes, - ), - ], - }); -}); - -connection.onDocumentFormatting(async (params): Promise => { - const uri = params.textDocument.uri; - const textDocument = documents.get(uri); - if (textDocument == null) { - return []; - } - - return formatText(uri, textDocument, textDocument.version, []); -}); - -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection); - -// Listen on the connection -connection.listen(); +// Node.js から uroborosql-language-server の LSP を起動するだけの薄いラッパー。 +// 標準入出力へバインドされるため、VS Code 側からは stdio transport で接続する。 +runLanguageServer();