Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for linting while typing #151

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
28 changes: 22 additions & 6 deletions src/ameba.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
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');
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
}

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

Expand All @@ -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();
})
Expand Down
31 changes: 28 additions & 3 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
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';

nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved

export interface AmebaConfig {
command: string;
configFileName: string;
onSave: boolean;
trigger: LintTrigger;
}

export enum LintTrigger {
None = "none",
Save = "save",
Type = "type"
}

export function getConfig(): AmebaConfig {
let command = 'ameba';
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<LintTrigger>("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
};
};
29 changes: 26 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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' && ameba.config.trigger !== LintTrigger.None) {
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
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);
}
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
}
});

workspace.onDidChangeTextDocument(e => {
if (ameba && ameba.config.trigger == LintTrigger.Type && e.document.languageId === 'crystal') {
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved
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") {
Expand Down Expand Up @@ -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'
}
nobodywasishere marked this conversation as resolved.
Show resolved Hide resolved

export function isDocumentVirtual(document: TextDocument): boolean {
return document.isDirty || document.isUntitled || document.uri.scheme !== 'file'
}