diff --git a/.tours/intro.tour b/.tours/intro.tour deleted file mode 100644 index 81f217f..0000000 --- a/.tours/intro.tour +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://aka.ms/codetour-schema", - "title": "Intro", - "steps": [ - { - "file": "README.md", - "description": "まずはスケルトンプロジェクトがどのような構造になっているか、順番に見ていきます。", - "line": 1 - } - ], - "ref": "master" -} \ No newline at end of file diff --git a/.tours/linter.tour b/.tours/linter.tour new file mode 100644 index 0000000..a0d10de --- /dev/null +++ b/.tours/linter.tour @@ -0,0 +1,87 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Linter", + "steps": [ + { + "file": "server/src/server.ts", + "description": "正規表現を用いて,2文字以上の大文字を検出する.", + "line": 100, + "selection": { + "start": { + "line": 72, + "character": 1 + }, + "end": { + "line": 100, + "character": 3 + } + }, + "title": "コード検証機能の編集" + }, + { + "file": "server/src/server.ts", + "description": "CodeActionなど修正コマンドで利用するモジュールを取得する", + "line": 15, + "selection": { + "start": { + "line": 4, + "character": 1 + }, + "end": { + "line": 15, + "character": 33 + } + }, + "title": "利用するモジュールの取得" + }, + { + "file": "server/src/server.ts", + "description": "サーバーの初期化でコードをワンクリックで修正する`QuickFix`を定義する.", + "line": 59, + "selection": { + "start": { + "line": 53, + "character": 1 + }, + "end": { + "line": 59, + "character": 6 + } + }, + "title": "サーバー初期化設定の編集" + }, + { + "file": "server/src/server.ts", + "description": "コードに対して関数を実行するCodeActionのリストを定義", + "line": 149, + "selection": { + "start": { + "line": 149, + "character": 1 + }, + "end": { + "line": 149, + "character": 40 + } + }, + "title": "CodeActionの宣言" + }, + { + "file": "server/src/server.ts", + "description": "生成した各警告に該当する文字を小文字に修正するCode Actionsを定義する.", + "line": 176, + "selection": { + "start": { + "line": 150, + "character": 1 + }, + "end": { + "line": 176, + "character": 6 + } + }, + "title": "CodeAction内容の定義" + } + ], + "ref": "linter" +} \ No newline at end of file diff --git a/README.md b/README.md index 3826c5a..a4188ff 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,12 @@ -# vscode-language-server-template README +# vscode-language-server-template Linter版 README -"vscode-language-server-template"のREADMEテンプレートです。 -実装の内容に従って以下のコンテンツを追加してください。 +"vscode-language-server-template"を用いたLinterを作成します。 -## 機能 - -拡張機能の特徴を記入してください。 -可能であればスクリーンショットを記載してください。 - -> Tip: 多くの人気拡張機能は動画を用意しています。利用動画があると使い方を直感的に理解できるようになります。 - -## 必要環境 - -必要な依存関係や環境設定があればここに記入してください。 - -## 設定 - -拡張機能で利用する設定があればここに記入してください。 +![Usage](./usage.gif) -例: - -本拡張機能は以下を[設定](https://code.visualstudio.com/docs/getstarted/settings)可能です: - -* `sampleserver.enable`: 拡張機能を 有効/無効 にする。標準では有効になっています。 - -## コマンド - -コマンドを作成している場合はここに記入してください。 - -例: - -本拡張機能は以下のコマンドから呼び出すことができます: +## 機能 -`Fix all auto-fixable problems`: ソースコードを上の警告を修正する。 +* コード検証機能 + * 3文字以上の大文字を検出します。 +* コード修正機能 + * 警告箇所を小文字に修正します diff --git a/server/src/server.ts b/server/src/server.ts index cd9da39..e3ef2cb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,16 +1,26 @@ 'use strict'; import { + CodeAction, + CodeActionKind, createConnection, Diagnostic, DiagnosticSeverity, ProposedFeatures, Range, TextDocuments, + TextDocumentEdit, TextDocumentSyncKind, + TextEdit, + VersionedTextDocumentIdentifier } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; +// コマンド識別子 +namespace CommandIDs { + export const fix = 'sample.fix'; +} + // サーバー接続オブジェクトを作成する。この接続にはNodeのIPC(プロセス間通信)を利用する // LSPの全機能を提供する const connection = createConnection(ProposedFeatures.all); @@ -39,7 +49,14 @@ connection.onInitialize((_params, _cancel, progress) => { save: { includeText: false, } - } + }, + codeActionProvider: { + codeActionKinds: [CodeActionKind.QuickFix], + resolveProvider: true + }, + executeCommandProvider: { + commands: [CommandIDs.fix], + }, }, }; }); @@ -51,23 +68,36 @@ connection.onInitialize((_params, _cancel, progress) => { function validate(doc: TextDocument) { // 警告などの状態を管理するリスト const diagnostics: Diagnostic[] = []; - // 0行目(エディタ上の行番号は1から)の端から端までに警告 - const range: Range = {start: {line: 0, character: 0}, - end: {line: 0, character: Number.MAX_VALUE}}; - // 警告を追加する - const diagnostic: Diagnostic = { - // 警告範囲 - range: range, - // 警告メッセージ - message: 'Hello world', - // 警告の重要度、Error, Warning, Information, Hintのいずれかを選ぶ - severity: DiagnosticSeverity.Warning, - // 警告コード、警告コードを識別するために使用する - code: '', - // 警告を発行したソース、例: eslint, typescript - source: 'sample', - }; - diagnostics.push(diagnostic); + + // 2つ以上並んでいるアルファベット大文字を検出 + const text = doc.getText(); + // 検出するための正規表現 (正規表現テスト: https://regex101.com/r/wXZbr9/1) + const pattern = /\b[A-Z]{2,}\b/g; + let m: RegExpExecArray | null; + + // 正規表現にマッチした文字列すべてを対象にする + while ((m = pattern.exec(text)) !== null) { + // 対象の位置から正規表現にマッチした文字までを対象にする + const range: Range = { + start: doc.positionAt(m.index), + end: doc.positionAt(m.index + m[0].length), + }; + // 警告を追加する; + const diagnostic: Diagnostic = { + // 警告範囲 + range: range, + // 警告メッセージ + message: `${m[0]} is all UPPERCASE.`, + // 警告の重要度、Error, Warning, Information, Hintのいずれかを選ぶ + severity: DiagnosticSeverity.Warning, + // 警告コード、警告コードを識別するために使用する + code: '', + // 警告を発行したソース、例: eslint, typescript + source: 'sample', + }; + // 警告リストに警告内容を追加 + diagnostics.push(diagnostic); + } //接続に警告を通知する connection.sendDiagnostics({ uri: doc.uri, diagnostics }); } @@ -102,6 +132,51 @@ function setupDocumentsListeners() { // 警告を削除する connection.sendDiagnostics({ uri: uri, diagnostics: []}); }); + + // Code Actionを追加する + connection.onCodeAction((params) => { + // sampleから生成した警告のみを対象とする + const diagnostics = params.context.diagnostics.filter((diag) => diag.source === 'sample'); + + connection.console.info(`Code Actions is added`); + // 対象ファイルを取得する + const textDocument = documents.get(params.textDocument.uri); + + // 対象ファイルが存在しない場合は終了する + if (textDocument === undefined || diagnostics.length === 0) { + return []; + } + const codeActions: CodeAction[] = []; + // 各警告に対してアクションを生成する + diagnostics.forEach((diag) => { + // アクションのメッセージ + const title = 'Fix to lower case'; + // 警告範囲の文字列取得 + const originalText = textDocument.getText(diag.range); + // 該当箇所を小文字に変更 + const edits = [TextEdit.replace(diag.range, originalText.toLowerCase())]; + + const textDocumentIdentifier: VersionedTextDocumentIdentifier = { + uri: textDocument.uri, + version: textDocument.version}; + const editPattern = { + documentChanges: [ + TextDocumentEdit.create(textDocumentIdentifier, edits) + ] + }; + // コードアクションを生成 + const fixAction = CodeAction.create( + title, + editPattern, + CodeActionKind.QuickFix); + // コードアクションと警告を関連付ける + fixAction.diagnostics = [diag]; + fixAction.isPreferred = true; + codeActions.push(fixAction); + }); + + return codeActions; + }); } // Listen on the connection diff --git a/usage.gif b/usage.gif new file mode 100644 index 0000000..ecbc9d1 Binary files /dev/null and b/usage.gif differ