From 222b1e47b8bc17fcd98c851789179d0768ea053c Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Sat, 23 Nov 2024 20:32:49 -0800 Subject: [PATCH 01/13] Functionality to lint while typing Added configuration option `lint-trigger` which defaults to save or type depending on the current version of ameba in use. This will also lint untitled files that aren't saved to disk. --- package.json | 22 +++++++++++++++++++++- src/ameba.ts | 28 ++++++++++++++++++++++------ src/configuration.ts | 31 ++++++++++++++++++++++++++++--- src/extension.ts | 29 ++++++++++++++++++++++++++--- 4 files changed, 97 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index d84d1ce..5c278cc 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,23 @@ "command": "crystal.ameba.disable", "title": "Crystal Ameba: disable lints (workspace)" } - ] + ], + "configuration": { + "type": "object", + "title": "Crystal Ameba configuration", + "properties": { + "crystal-ameba.lint-trigger": { + "type": "string", + "description": "When the linter should be executed. Set to `none` to disable automatic linting.", + "default": "type", + "enum": [ + "none", + "save", + "type" + ] + } + } + } }, "scripts": { "vscode:prepublish": "yarn run compile", @@ -55,8 +71,12 @@ "@types/mocha": "^10.0.9", "@types/node": "^22.9.0", "@types/vscode": "^1.75.0", + "@types/semver": "^7.5.8", "tslint": "^6.1.3", "typescript": "^5.6.3", "vscode-test": "^1.6.1" + }, + "dependencies": { + "semver": "^7.6.3" } } diff --git a/src/ameba.ts b/src/ameba.ts index 1807628..5869737 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -16,7 +16,7 @@ import { import { AmebaOutput } from './amebaOutput'; import { AmebaConfig, getConfig } from './configuration'; import { Task, TaskQueue } from './taskQueue'; -import { outputChannel } from './extension'; +import { isDocumentVirtual, noWorkspaceFolder, outputChannel } from './extension'; export class Ameba { private diag: DiagnosticCollection; @@ -29,13 +29,23 @@ export class Ameba { this.config = getConfig(); } - public execute(document: TextDocument): void { - if (document.languageId !== 'crystal' || document.isUntitled || document.uri.scheme !== 'file') { - return; + public execute(document: TextDocument, virtual: boolean = false): void { + if (document.languageId !== 'crystal') return; + if (isDocumentVirtual(document) && !virtual) return; + + const dir = (workspace.getWorkspaceFolder(document.uri) ?? noWorkspaceFolder(document.uri)).uri.fsPath; + + const args = [this.config.command, '--format', 'json']; + + if (!virtual) { + args.push(document.fileName) + } else { + args.push('--stdin-filename', document.fileName); + + // Disabling these as they're common when typing + args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace,Naming/Filename'); } - const args = [this.config.command, document.fileName, '--format', 'json']; - const dir = workspace.getWorkspaceFolder(document.uri)!.uri.fsPath; const configFile = path.join(dir, this.config.configFileName); if (existsSync(configFile)) args.push('--config', configFile); @@ -47,6 +57,12 @@ export class Ameba { outputChannel.appendLine(`$ ${args.join(' ')}`) const proc = spawn(args[0], args.slice(1), { cwd: dir }); + if (virtual) { + const documentText: string = document.getText(); + proc.stdin.write(documentText) + proc.stdin.end(); + } + token.onCancellationRequested(_ => { proc.kill(); }) diff --git a/src/configuration.ts b/src/configuration.ts index f09f25e..22b0596 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,11 +1,22 @@ import { workspace } from 'vscode'; import * as path from 'path'; import { existsSync } from 'fs'; +import * as semver from 'semver'; +import { execSync } from 'child_process'; + +import { outputChannel } from './extension'; + export interface AmebaConfig { command: string; configFileName: string; - onSave: boolean; + trigger: LintTrigger; +} + +export enum LintTrigger { + None = "none", + Save = "save", + Type = "type" } export function getConfig(): AmebaConfig { @@ -13,12 +24,26 @@ export function getConfig(): AmebaConfig { const root = workspace.workspaceFolders || []; if (root.length) { const localAmebaPath = path.join(root[0].uri.fsPath, 'bin', 'ameba'); - if (existsSync(localAmebaPath)) command = localAmebaPath; + if (existsSync(localAmebaPath)) { + outputChannel.appendLine(`[Config] Using local ameba at ${localAmebaPath}`) + command = localAmebaPath; + } else { + outputChannel.appendLine(`[Config] Using system ameba`) + } + } + + const workspaceConfig = workspace.getConfiguration('crystal-ameba'); + const currentVersion = execSync(`"${command}" --version`).toString(); + + let trigger = workspaceConfig.get("lint-trigger", LintTrigger.Type); + + if (!semver.satisfies(currentVersion, ">=1.6.4") && trigger == LintTrigger.Type) { + trigger = LintTrigger.Save; } return { command, configFileName: '.ameba.yml', - onSave: true + trigger: trigger }; }; diff --git a/src/extension.ts b/src/extension.ts index 37080d7..141790d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,7 +7,7 @@ import { import * as path from 'path' import { Ameba } from './ameba'; -import { getConfig } from './configuration'; +import { getConfig, LintTrigger } from './configuration'; export let outputChannel: OutputChannel; @@ -18,6 +18,7 @@ export function activate(context: ExtensionContext) { const diag = languages.createDiagnosticCollection('crystal'); let ameba: Ameba | null = new Ameba(diag); + context.subscriptions.push(diag); context.subscriptions.push( @@ -83,12 +84,30 @@ export function activate(context: ExtensionContext) { executeAmebaOnWorkspace(ameba); + // This can happen when a file is open _or_ when a file's language id changes workspace.onDidOpenTextDocument(doc => { - ameba && ameba.execute(doc); + if (ameba && doc.languageId === 'crystal') { + if (isDocumentVirtual(doc)) { + if (ameba.config.trigger === LintTrigger.Type) { + outputChannel.appendLine(`[Open] Running ameba on ${getRelativePath(doc)}`); + ameba.execute(doc, true); + } + } else { + outputChannel.appendLine(`[Open] Running ameba on ${getRelativePath(doc)}`); + ameba.execute(doc); + } + } }); + workspace.onDidChangeTextDocument(e => { + if (ameba && ameba.config.trigger == LintTrigger.Type && e.document.languageId === 'crystal') { + outputChannel.appendLine(`[Change] Running ameba on ${getRelativePath(e.document)}`); + ameba.execute(e.document, true); + } + }) + workspace.onDidSaveTextDocument(doc => { - if (ameba && ameba.config.onSave && isValidCrystalDocument(doc)) { + if (ameba && ameba.config.trigger === LintTrigger.Save && isValidCrystalDocument(doc)) { outputChannel.appendLine(`[Save] Running ameba on ${getRelativePath(doc)}`) ameba.execute(doc); } else if (ameba && path.basename(doc.fileName) == ".ameba.yml") { @@ -133,3 +152,7 @@ export function noWorkspaceFolder(uri: Uri): WorkspaceFolder { function isValidCrystalDocument(doc: TextDocument): boolean { return doc.languageId === 'crystal' && !doc.isUntitled && doc.uri.scheme === 'file' } + +export function isDocumentVirtual(document: TextDocument): boolean { + return document.isDirty || document.isUntitled || document.uri.scheme !== 'file' +} From 489bef3e02264d2d6106a84e37dd710f3e5edda0 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Sun, 24 Nov 2024 03:50:41 -0500 Subject: [PATCH 02/13] Don't run linter when lint type is set to none --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 141790d..8ef9f0f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,7 +86,7 @@ export function activate(context: ExtensionContext) { // This can happen when a file is open _or_ when a file's language id changes workspace.onDidOpenTextDocument(doc => { - if (ameba && doc.languageId === 'crystal') { + if (ameba && doc.languageId === 'crystal' && ameba.config.trigger !== LintTrigger.None) { if (isDocumentVirtual(doc)) { if (ameba.config.trigger === LintTrigger.Type) { outputChannel.appendLine(`[Open] Running ameba on ${getRelativePath(doc)}`); From a2a235f78eb12b4fe86594b013cdb2ba31637ec8 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Fri, 29 Nov 2024 17:03:32 -0500 Subject: [PATCH 03/13] Update extension.ts Co-authored-by: Sijawusz Pur Rahnama --- src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 8ef9f0f..b9f109a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -149,8 +149,8 @@ export function noWorkspaceFolder(uri: Uri): WorkspaceFolder { } } -function isValidCrystalDocument(doc: TextDocument): boolean { - return doc.languageId === 'crystal' && !doc.isUntitled && doc.uri.scheme === 'file' +function isCrystalDocument(doc: TextDocument): boolean { + return doc.languageId === 'crystal' } export function isDocumentVirtual(document: TextDocument): boolean { From c7566537805d093de982f8965fc257904b247668 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Fri, 29 Nov 2024 17:03:39 -0500 Subject: [PATCH 04/13] Update extension.ts Co-authored-by: Sijawusz Pur Rahnama --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index b9f109a..ecb71ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -100,7 +100,7 @@ export function activate(context: ExtensionContext) { }); workspace.onDidChangeTextDocument(e => { - if (ameba && ameba.config.trigger == LintTrigger.Type && e.document.languageId === 'crystal') { + if (ameba && ameba.config.trigger == LintTrigger.Type && isCrystalDocument(e.document)) { outputChannel.appendLine(`[Change] Running ameba on ${getRelativePath(e.document)}`); ameba.execute(e.document, true); } From 165a6fd9f765eb6163eb122e30f4783739d92b8f Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Fri, 29 Nov 2024 17:03:49 -0500 Subject: [PATCH 05/13] Update extension.ts Co-authored-by: Sijawusz Pur Rahnama --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index ecb71ac..94b2f1b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,7 +86,7 @@ export function activate(context: ExtensionContext) { // This can happen when a file is open _or_ when a file's language id changes workspace.onDidOpenTextDocument(doc => { - if (ameba && doc.languageId === 'crystal' && ameba.config.trigger !== LintTrigger.None) { + if (ameba && ameba.config.trigger !== LintTrigger.None && isCrystalDocument(doc)) { if (isDocumentVirtual(doc)) { if (ameba.config.trigger === LintTrigger.Type) { outputChannel.appendLine(`[Open] Running ameba on ${getRelativePath(doc)}`); From e59c022a528e4aff109e4c9a017ae87c95942b49 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Fri, 29 Nov 2024 17:03:55 -0500 Subject: [PATCH 06/13] Update configuration.ts Co-authored-by: Sijawusz Pur Rahnama --- src/configuration.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/configuration.ts b/src/configuration.ts index 22b0596..4c707e4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -6,7 +6,6 @@ import { execSync } from 'child_process'; import { outputChannel } from './extension'; - export interface AmebaConfig { command: string; configFileName: string; From 9345e569b55870dbf39cbe61787a55b2e5af1de2 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Fri, 29 Nov 2024 17:04:03 -0500 Subject: [PATCH 07/13] Update ameba.ts Co-authored-by: Sijawusz Pur Rahnama --- src/ameba.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ameba.ts b/src/ameba.ts index 5869737..45b7624 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -30,7 +30,7 @@ export class Ameba { } public execute(document: TextDocument, virtual: boolean = false): void { - if (document.languageId !== 'crystal') return; + if (!isCrystalDocument(document)) return; if (isDocumentVirtual(document) && !virtual) return; const dir = (workspace.getWorkspaceFolder(document.uri) ?? noWorkspaceFolder(document.uri)).uri.fsPath; From e5a71d1b1111c8b9d31c24946298b52565759062 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Sun, 1 Dec 2024 17:53:00 -0500 Subject: [PATCH 08/13] Fix error --- src/ameba.ts | 2 +- src/extension.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ameba.ts b/src/ameba.ts index 45b7624..00478ca 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -16,7 +16,7 @@ import { import { AmebaOutput } from './amebaOutput'; import { AmebaConfig, getConfig } from './configuration'; import { Task, TaskQueue } from './taskQueue'; -import { isDocumentVirtual, noWorkspaceFolder, outputChannel } from './extension'; +import { isCrystalDocument, isDocumentVirtual, noWorkspaceFolder, outputChannel } from './extension'; export class Ameba { private diag: DiagnosticCollection; diff --git a/src/extension.ts b/src/extension.ts index 94b2f1b..3ee4423 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -107,7 +107,7 @@ export function activate(context: ExtensionContext) { }) workspace.onDidSaveTextDocument(doc => { - if (ameba && ameba.config.trigger === LintTrigger.Save && isValidCrystalDocument(doc)) { + if (ameba && ameba.config.trigger === LintTrigger.Save && isCrystalDocument(doc)) { outputChannel.appendLine(`[Save] Running ameba on ${getRelativePath(doc)}`) ameba.execute(doc); } else if (ameba && path.basename(doc.fileName) == ".ameba.yml") { @@ -128,7 +128,7 @@ function executeAmebaOnWorkspace(ameba: Ameba | null) { if (!ameba) return; for (const doc of workspace.textDocuments) { - if (isValidCrystalDocument(doc)) { + if (isCrystalDocument(doc)) { outputChannel.appendLine(`[Workspace] Running ameba on ${getRelativePath(doc)}`); ameba.execute(doc); } @@ -149,7 +149,7 @@ export function noWorkspaceFolder(uri: Uri): WorkspaceFolder { } } -function isCrystalDocument(doc: TextDocument): boolean { +export function isCrystalDocument(doc: TextDocument): boolean { return doc.languageId === 'crystal' } From d61838db293322285addef936a7dbc83be7555d4 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Mon, 2 Dec 2024 10:08:09 -0500 Subject: [PATCH 09/13] Update src/extension.ts Co-authored-by: Sijawusz Pur Rahnama --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 3ee4423..b267fbb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -125,7 +125,7 @@ export function activate(context: ExtensionContext) { export function deactivate() { } function executeAmebaOnWorkspace(ameba: Ameba | null) { - if (!ameba) return; + if (!ameba || ameba.config.trigger === LintTrigger.None) return; for (const doc of workspace.textDocuments) { if (isCrystalDocument(doc)) { From fcc2d7ed6bd3be42861c23499bcc4f6f2189a778 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Mon, 2 Dec 2024 10:12:10 -0500 Subject: [PATCH 10/13] Use `-` instead of `--stdin-filename` --- src/ameba.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ameba.ts b/src/ameba.ts index 00478ca..9c2c085 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -40,7 +40,8 @@ export class Ameba { if (!virtual) { args.push(document.fileName) } else { - args.push('--stdin-filename', document.fileName); + // Indicate that the source is passed through STDIN + args.push('-'); // Disabling these as they're common when typing args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace,Naming/Filename'); @@ -164,7 +165,9 @@ export class Ameba { } let diagnosticUri: Uri; - if (path.isAbsolute(source.path)) { + if (virtual) { + diagnosticUri = document.uri; + } else if (path.isAbsolute(source.path)) { diagnosticUri = Uri.parse(source.path) } else if (document.isUntitled) { diagnosticUri = document.uri; From f1158b9f770491fc2d6fc213f7b277126308db7f Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Mon, 2 Dec 2024 10:23:58 -0500 Subject: [PATCH 11/13] Properly handle virtual files --- src/extension.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index b267fbb..b06a086 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -102,7 +102,7 @@ export function activate(context: ExtensionContext) { workspace.onDidChangeTextDocument(e => { if (ameba && ameba.config.trigger == LintTrigger.Type && isCrystalDocument(e.document)) { outputChannel.appendLine(`[Change] Running ameba on ${getRelativePath(e.document)}`); - ameba.execute(e.document, true); + ameba.execute(e.document, isDocumentVirtual(e.document)); } }) @@ -110,7 +110,7 @@ export function activate(context: ExtensionContext) { if (ameba && ameba.config.trigger === LintTrigger.Save && isCrystalDocument(doc)) { outputChannel.appendLine(`[Save] Running ameba on ${getRelativePath(doc)}`) ameba.execute(doc); - } else if (ameba && path.basename(doc.fileName) == ".ameba.yml") { + } else if (ameba && ameba.config.trigger !== LintTrigger.None && path.basename(doc.fileName) == ".ameba.yml") { outputChannel.appendLine(`[Config] Reloading diagnostics after config file change`) ameba.clear(); executeAmebaOnWorkspace(ameba); @@ -129,8 +129,15 @@ function executeAmebaOnWorkspace(ameba: Ameba | null) { for (const doc of workspace.textDocuments) { if (isCrystalDocument(doc)) { - outputChannel.appendLine(`[Workspace] Running ameba on ${getRelativePath(doc)}`); - ameba.execute(doc); + if (isDocumentVirtual(doc)) { + if (ameba.config.trigger === LintTrigger.Type) { + outputChannel.appendLine(`[Workspace] Running ameba on ${getRelativePath(doc)}`); + ameba.execute(doc, true); + } + } else { + outputChannel.appendLine(`[Workspace] Running ameba on ${getRelativePath(doc)}`); + ameba.execute(doc); + } } } } From 7b9cbd09de7ac6e3a25aadfec704ed43ef3e170e Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Mon, 2 Dec 2024 13:07:51 -0500 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: Sijawusz Pur Rahnama --- src/ameba.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ameba.ts b/src/ameba.ts index 9c2c085..d420436 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -44,7 +44,7 @@ export class Ameba { args.push('-'); // Disabling these as they're common when typing - args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace,Naming/Filename'); + args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace'); } const configFile = path.join(dir, this.config.configFileName); @@ -169,8 +169,6 @@ export class Ameba { diagnosticUri = document.uri; } else if (path.isAbsolute(source.path)) { diagnosticUri = Uri.parse(source.path) - } else if (document.isUntitled) { - diagnosticUri = document.uri; } else { diagnosticUri = Uri.parse(path.join(dir, source.path)); } From 50f4c5e1e8bb24e0ff727536f3922c856ff19436 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Tue, 3 Dec 2024 13:19:19 -0500 Subject: [PATCH 13/13] Update src/ameba.ts Co-authored-by: Sijawusz Pur Rahnama --- src/ameba.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ameba.ts b/src/ameba.ts index d420436..bf69edc 100644 --- a/src/ameba.ts +++ b/src/ameba.ts @@ -40,11 +40,11 @@ export class Ameba { if (!virtual) { args.push(document.fileName) } else { - // Indicate that the source is passed through STDIN - args.push('-'); - // Disabling these as they're common when typing args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace'); + + // Indicate that the source is passed through STDIN + args.push('-'); } const configFile = path.join(dir, this.config.configFileName);