diff --git a/package.json b/package.json index 4fab85b..8875ada 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "vscode": "^1.25.0" }, "activationEvents": [ - "onLanguage:plaintext" + "onLanguage:elm" ], "main": "./client/out/extension", "contributes": { diff --git a/server/src/elmLinter.ts b/server/src/elmLinter.ts new file mode 100644 index 0000000..a02bbca --- /dev/null +++ b/server/src/elmLinter.ts @@ -0,0 +1,182 @@ +import * as cp from 'child_process'; +import * as readline from 'readline'; +import * as utils from './elmUtils'; +import * as vscode from 'vscode-languageserver/lib/main'; +import Uri from 'vscode-uri/lib/umd'; +import { Diagnostic } from 'vscode-languageserver/lib/main'; + +export interface IElmIssueRegion { + start: { line: number; column: number }; + end: { line: number; column: number }; +} + +export interface IElmIssue { + tag: string; + overview: string; + subregion: string; + details: string; + region: IElmIssueRegion; + type: string; + file: string; +} + +function severityStringToDiagnosticSeverity( + severity: string, +): vscode.DiagnosticSeverity { + switch (severity) { + case 'error': + return vscode.DiagnosticSeverity.Error; + case 'warning': + return vscode.DiagnosticSeverity.Warning; + default: + return vscode.DiagnosticSeverity.Error; + } +} + +function elmMakeIssueToDiagnostic(issue: IElmIssue): vscode.Diagnostic { + let lineRange: vscode.Range = vscode.Range.create( + issue.region.start.line - 1, + issue.region.start.column - 1, + issue.region.end.line - 1, + issue.region.end.column - 1, + ); + return vscode.Diagnostic.create( + lineRange, + issue.overview + ' - ' + issue.details.replace(/\[\d+m/g, ''), + severityStringToDiagnosticSeverity(issue.type), + ); +} + +function checkForErrors(connection: vscode.Connection, rootPath: string, filename: string): Promise { + return new Promise((resolve, reject) => { + const makeCommand: string = 'elm-make'; + const cwd: string = + utils.detectProjectRoot(filename) || rootPath; + let make: cp.ChildProcess; + if (utils.isWindows) { + filename = "\"" + filename + "\"" + } + const args = [filename, '--report', 'json', '--output', '/dev/null']; + if (utils.isWindows) { + make = cp.exec(makeCommand + ' ' + args.join(' '), { cwd: cwd }); + } else { + make = cp.spawn(makeCommand, args, { cwd: cwd }); + } + // output is actually optional + // (fixed in https://github.com/Microsoft/vscode/commit/b4917afe9bdee0e9e67f4094e764f6a72a997c70, + // but unreleased at this time) + const stdoutlines: readline.ReadLine = readline.createInterface({ + input: make.stdout, + output: undefined, + }); + const lines: IElmIssue[] = []; + stdoutlines.on('line', (line: string) => { + // Ignore compiler success. + if (line.startsWith('Successfully generated')) { + return; + } + // Elm writes out JSON arrays of diagnostics, with one array per line. + // Multiple lines may be received. + lines.push(...(JSON.parse(line))); + }); + const stderr: Buffer[] = []; + make.stderr.on('data', (data: Buffer) => { + if (data) { + stderr.push(data); + } + }); + make.on('error', (err: Error) => { + stdoutlines.close(); + if (err && (err).code === 'ENOENT') { + connection.console.log( + "The 'elm-make' compiler is not available. Install Elm from http://elm-lang.org/.", + ); + resolve([]); + } else { + reject(err); + } + }); + make.on('close', (code: number, signal: string) => { + stdoutlines.close(); + if (stderr.length) { + let errorResult: IElmIssue = { + tag: 'error', + overview: '', + subregion: '', + details: stderr.join(''), + region: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + type: 'error', + file: filename, + }; + resolve([errorResult]); + } else { + resolve(lines); + } + }); + }); +} + +export function runLinter( + connection: vscode.Connection, + rootPath: string, + document: vscode.TextDocument, + // elmAnalyse: ElmAnalyse, +): void { + let compileErrors: vscode.Diagnostic[] = []; + let uri: Uri = Uri.parse(document.uri); + + checkForErrors(connection, rootPath, uri.fsPath) + .then((compilerErrors: IElmIssue[]) => { + const cwd: string = + utils.detectProjectRoot(uri.fsPath) || rootPath; + let splitCompilerErrors: Map = new Map(); + + compilerErrors.forEach((issue: IElmIssue) => { + // If provided path is relative, make it absolute + if (issue.file.startsWith('.')) { + issue.file = cwd + issue.file.slice(1); + } + if (splitCompilerErrors.has(issue.file)) { + splitCompilerErrors.get(issue.file).push(issue); + } else { + splitCompilerErrors.set(issue.file, [issue]); + } + }); + // Turn split arrays into diagnostics and associate them with correct files in VS + splitCompilerErrors.forEach((issue: IElmIssue[], path: string) => { + connection.sendDiagnostics({ + uri: document.uri, + diagnostics: issue.map(error => elmMakeIssueToDiagnostic(error)), + }); + }); + }) + .catch(error => { + }); + + // if (elmAnalyse.elmAnalyseIssues.length > 0) { + // let splitCompilerErrors: Map = new Map(); + // elmAnalyse.elmAnalyseIssues.forEach((issue: IElmIssue) => { + // if (splitCompilerErrors.has(issue.file)) { + // splitCompilerErrors.get(issue.file).push(issue); + // } else { + // splitCompilerErrors.set(issue.file, [issue]); + // } + // splitCompilerErrors.forEach( + // (analyserIssue: IElmIssue[], path: string) => { + // compileErrors.concat( + // analyserIssue.map(error => elmMakeIssueToDiagnostic(error)), + // ); + // }, + // ); + // }); + // } +} diff --git a/server/src/elmUtils.ts b/server/src/elmUtils.ts new file mode 100644 index 0000000..d0e1c87 --- /dev/null +++ b/server/src/elmUtils.ts @@ -0,0 +1,67 @@ +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode-languageserver'; + +export const isWindows = process.platform === 'win32'; + +/** Options for execCmd */ +export interface ExecCmdOptions { + /** The project root folder for this file is used as the cwd of the process */ + fileName?: string; + /** Any arguments */ + cmdArguments?: string[]; + /** Shows a message if an error occurs (in particular the command not being */ + /* found), instead of rejecting. If this happens, the promise never resolves */ + showMessageOnError?: boolean; + /** Called after the process successfully starts */ + onStart?: () => void; + /** Called when data is sent to stdout */ + onStdout?: (data: string) => void; + /** Called when data is sent to stderr */ + onStderr?: (data: string) => void; + /** Called after the command (successfully or unsuccessfully) exits */ + onExit?: () => void; + /** Text to add when command is not found (maybe helping how to install) */ + notFoundText?: string; +} + + +export function findProj(dir: string): string { + if (fs.lstatSync(dir).isDirectory()) { + const files = fs.readdirSync(dir); + const file = files.find((v, i) => v === 'elm-package.json'); + if (file !== undefined) { + return dir + path.sep + file; + } + let parent = ''; + if (dir.lastIndexOf(path.sep) > 0) { + parent = dir.substr(0, dir.lastIndexOf(path.sep)); + } + if (parent === '') { + return ''; + } else { + return findProj(parent); + } + } +} + +export function detectProjectRoot(fileName: string): string { + const proj = findProj(path.dirname(fileName)); + if (proj !== '') { + return path.dirname(proj); + } + return undefined; +} + +export function getIndicesOf(searchStr: string, str: string): number[] { + let startIndex = 0, + searchStrLen = searchStr.length; + let index, + indices = []; + while ((index = str.indexOf(searchStr, startIndex)) > -1) { + indices.push(index); + startIndex = index + searchStrLen; + } + return indices; +} diff --git a/server/src/server.ts b/server/src/server.ts index 98c69e1..3ed8174 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,6 +1,6 @@ 'use strict'; import * as cp from 'child_process'; -import Uri from 'vscode-uri' +import Uri from 'vscode-uri/lib/umd' const Compiler = require('node-elm-compiler'); import { createConnection, @@ -18,6 +18,8 @@ import { Position, TextEdit } from 'vscode-languageserver'; +import { runLinter, IElmIssue } from './elmLinter'; +// import { ElmAnalyse } from './elmAnalyse'; // Create a connection for the server. The connection uses Node's IPC as a transport. // Also include all preview / proposed LSP features. @@ -29,9 +31,11 @@ let documents: TextDocuments = new TextDocuments(); let hasConfigurationCapability: boolean = false; let hasWorkspaceFolderCapability: boolean = false; +let rootPath: string = undefined; connection.onInitialize((params: InitializeParams) => { let capabilities = params.capabilities; + this.rootPath = params.rootPath; // Does the client support the `workspace/configuration` request? // If not, we will fall back using global settings @@ -66,12 +70,20 @@ connection.onInitialized(() => { } }); + documents.onDidOpen(params => { - validateTextDocument(params.document); + const elmAnalyseIssues: IElmIssue[] = []; + // const elmAnalyse = new ElmAnalyse(elmAnalyseIssues); + runLinter(connection, this.rootPath, params.document); + // validateTextDocument(params.document); }); documents.onDidSave(params => { - validateTextDocument(params.document); + // const elmAnalyseIssues: IElmIssue[] = []; + // const elmAnalyse = new ElmAnalyse(elmAnalyseIssues); + runLinter(connection, this.rootPath, params.document); + + // validateTextDocument(params.document); }); async function validateTextDocument(textDocument: TextDocument): Promise { @@ -81,7 +93,10 @@ async function validateTextDocument(textDocument: TextDocument): Promise { let diagnostics: Diagnostic[] = [] try { await Compiler.compileToString(uri.fsPath, { report: 'json' }) - + + var x = await Compiler.findAllDependencies(uri.fsPath); + connection.console.log(x); + } catch (err) { const issues = JSON.parse(err.message.split('\n')[1]); const byFile = issues.reduce((acc: any, issue: any) => {