diff --git a/packages/language-server/src/lib/documents/DocumentManager.ts b/packages/language-server/src/lib/documents/DocumentManager.ts index db421b44f..c30387be1 100644 --- a/packages/language-server/src/lib/documents/DocumentManager.ts +++ b/packages/language-server/src/lib/documents/DocumentManager.ts @@ -18,9 +18,11 @@ export class DocumentManager { public locked = new Set(); public deleteCandidates = new Set(); - constructor(private createDocument: (textDocument: TextDocumentItem) => Document) {} + constructor( + private createDocument: (textDocument: Pick) => Document, + ) {} - openDocument(textDocument: TextDocumentItem): Document { + openDocument(textDocument: Pick): Document { let document: Document; if (this.documents.has(textDocument.uri)) { document = this.documents.get(textDocument.uri)!; @@ -45,8 +47,9 @@ export class DocumentManager { } getAllOpenedByClient() { - return Array.from(this.documents.entries()) - .filter((doc) => this.openedInClient.has(doc[0])); + return Array.from(this.documents.entries()).filter((doc) => + this.openedInClient.has(doc[0]), + ); } releaseDocument(uri: string): void { @@ -58,7 +61,6 @@ export class DocumentManager { } } - closeDocument(uri: string) { const document = this.documents.get(uri); if (!document) { diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index eee4f5fef..599c83216 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -34,10 +34,8 @@ export class LSAndTSDocResolver { private createDocument = (fileName: string, content: string) => { const uri = pathToUrl(fileName); const document = this.docManager.openDocument({ - languageId: 'svelte', text: content, uri, - version: 0, }); this.docManager.lockDocument(uri); return document; diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 2fcdcd0ae..fa103ba9c 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -3,6 +3,7 @@ import { LSConfigManager } from './ls-config'; import { CSSPlugin, HTMLPlugin, PluginHost, SveltePlugin, TypeScriptPlugin } from './plugins'; import { Diagnostic } from 'vscode-languageserver'; import { Logger } from './logger'; +import { urlToPath } from './utils'; /** * Small wrapper around PluginHost's Diagnostic Capabilities @@ -30,17 +31,44 @@ export class SvelteCheck { } /** - * Gets diagnostics for a svelte file. + * Creates/updates given document * - * @param params Text and Uri of a svelte file + * @param doc Text and Uri of the document */ - async getDiagnostics(params: { text: string; uri: string }): Promise { + upsertDocument(doc: { text: string; uri: string }) { this.docManager.openDocument({ - languageId: 'svelte', - text: params.text, - uri: params.uri, - version: 1, + text: doc.text, + uri: doc.uri, }); - return await this.pluginHost.getDiagnostics({ uri: params.uri }); + this.docManager.markAsOpenedInClient(doc.uri); + } + + /** + * Removes/closes document + * + * @param uri Uri of the document + */ + removeDocument(uri: string) { + this.docManager.closeDocument(uri); + this.docManager.releaseDocument(uri); + } + + /** + * Gets the diagnostics for all currently open files. + */ + async getDiagnostics(): Promise< + { filePath: string; text: string; diagnostics: Diagnostic[] }[] + > { + return await Promise.all( + this.docManager.getAllOpenedByClient().map(async (doc) => { + const uri = doc[1].uri; + const diagnostics = await this.pluginHost.getDiagnostics({ uri }); + return { + filePath: urlToPath(uri) || '', + text: this.docManager.documents.get(uri)?.getText() || '', + diagnostics, + }; + }), + ); } } diff --git a/packages/language-server/test/lib/documents/DocumentManager.test.ts b/packages/language-server/test/lib/documents/DocumentManager.test.ts index 7e8327ee1..8420f101e 100644 --- a/packages/language-server/test/lib/documents/DocumentManager.test.ts +++ b/packages/language-server/test/lib/documents/DocumentManager.test.ts @@ -11,7 +11,7 @@ describe('Document Manager', () => { text: 'Hello, world!', }; - const createTextDocument = (textDocument: TextDocumentItem) => + const createTextDocument = (textDocument: Pick) => new Document(textDocument.uri, textDocument.text); it('opens documents', () => { diff --git a/packages/language-server/test/plugins/PluginHost.test.ts b/packages/language-server/test/plugins/PluginHost.test.ts index 9eb0f5233..7858008dd 100644 --- a/packages/language-server/test/plugins/PluginHost.test.ts +++ b/packages/language-server/test/plugins/PluginHost.test.ts @@ -13,10 +13,9 @@ describe('PluginHost', () => { }; function setup(pluginProviderStubs: T) { - const createTextDocument = (textDocument: TextDocumentItem) => - new Document(textDocument.uri, textDocument.text); - - const docManager = new DocumentManager(createTextDocument); + const docManager = new DocumentManager( + (textDocument) => new Document(textDocument.uri, textDocument.text), + ); const pluginHost = new PluginHost(docManager, {}); const plugin = { diff --git a/packages/svelte-check/README.md b/packages/svelte-check/README.md index 3adee5ad1..2781337b1 100644 --- a/packages/svelte-check/README.md +++ b/packages/svelte-check/README.md @@ -54,6 +54,12 @@ Usage: `--output ` +`--watch` Will not exit after one pass but keep watching files for changes and rerun diagnostics. + +`--ignore ` + +`--fail-on-warnings` Will also exit with error code when there are warnings + ### More docs, preprocessor setup and troubleshooting [See here](/docs/README.md). diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index e80163839..dd95204e2 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -20,6 +20,7 @@ "homepage": "https://github.com/sveltejs/language-tools#readme", "dependencies": { "chalk": "^4.0.0", + "chokidar": "^3.4.1", "glob": "^7.1.6", "minimist": "^1.2.5", "svelte-language-server": "*", diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index 75fafc76a..2c35cf69e 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -10,6 +10,7 @@ import { SvelteCheck } from 'svelte-language-server'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; import { URI } from 'vscode-uri'; import { HumanFriendlyWriter, MachineFriendlyWriter, Writer } from './writers'; +import { watch } from 'chokidar'; const outputFormats = ['human', 'human-verbose', 'machine'] as const; type OutputFormat = typeof outputFormats[number]; @@ -20,58 +21,105 @@ type Result = { warningCount: number; }; -async function getDiagnostics(workspaceUri: URI, writer: Writer): Promise { - writer.start(workspaceUri.fsPath); - - const svelteCheck = new SvelteCheck(workspaceUri.fsPath); - +function openAllDocuments( + workspaceUri: URI, + filePathsToIgnore: string[], + svelteCheck: SvelteCheck, +) { const files = glob.sync('**/*.svelte', { cwd: workspaceUri.fsPath, - ignore: ['node_modules/**'], + ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)), }); const absFilePaths = files.map((f) => path.resolve(workspaceUri.fsPath, f)); - const result = { - fileCount: absFilePaths.length, - errorCount: 0, - warningCount: 0, - }; - for (const absFilePath of absFilePaths) { const text = fs.readFileSync(absFilePath, 'utf-8'); + svelteCheck.upsertDocument({ + uri: URI.file(absFilePath).toString(), + text, + }); + } +} - let res: Diagnostic[] = []; +async function getDiagnostics( + workspaceUri: URI, + writer: Writer, + svelteCheck: SvelteCheck, +): Promise { + writer.start(workspaceUri.fsPath); - try { - res = await svelteCheck.getDiagnostics({ - uri: URI.file(absFilePath).toString(), - text, + try { + const diagnostics = await svelteCheck.getDiagnostics(); + + const result: Result = { + fileCount: diagnostics.length, + errorCount: 0, + warningCount: 0, + }; + + for (const diagnostic of diagnostics) { + writer.file( + diagnostic.diagnostics, + workspaceUri.fsPath, + path.relative(workspaceUri.fsPath, diagnostic.filePath), + diagnostic.text, + ); + + diagnostic.diagnostics.forEach((d: Diagnostic) => { + if (d.severity === DiagnosticSeverity.Error) { + result.errorCount += 1; + } else if (d.severity === DiagnosticSeverity.Warning) { + result.warningCount += 1; + } }); - } catch (err) { - writer.failure(err); - return null; } - writer.file( - res, - workspaceUri.fsPath, - path.relative(workspaceUri.fsPath, absFilePath), - text, - ); + writer.completion(result.fileCount, result.errorCount, result.warningCount); + return result; + } catch (err) { + writer.failure(err); + return null; + } +} - res.forEach((d: Diagnostic) => { - if (d.severity === DiagnosticSeverity.Error) { - result.errorCount += 1; - } else if (d.severity === DiagnosticSeverity.Warning) { - result.warningCount += 1; - } - }); +class DiagnosticsWatcher { + private updateDiagnostics: any; + + constructor( + private workspaceUri: URI, + private svelteCheck: SvelteCheck, + private writer: Writer, + filePathsToIgnore: string[], + ) { + watch(`${workspaceUri.fsPath}/**/*.svelte`, { + ignored: ['node_modules'] + .concat(filePathsToIgnore) + .map((ignore) => path.join(workspaceUri.fsPath, ignore)), + }) + .on('add', (path) => this.updateDocument(path)) + .on('unlink', (path) => this.removeDocument(path)) + .on('change', (path) => this.updateDocument(path)); } - writer.completion(result.fileCount, result.errorCount, result.warningCount); + private updateDocument(path: string) { + const text = fs.readFileSync(path, 'utf-8'); + this.svelteCheck.upsertDocument({ text, uri: URI.file(path).toString() }); + this.scheduleDiagnostics(); + } - return result; + private removeDocument(path: string) { + this.svelteCheck.removeDocument(URI.file(path).toString()); + this.scheduleDiagnostics(); + } + + private scheduleDiagnostics() { + clearTimeout(this.updateDiagnostics); + this.updateDiagnostics = setTimeout( + () => getDiagnostics(this.workspaceUri, this.writer, this.svelteCheck), + 1000, + ); + } } (async () => { @@ -99,12 +147,23 @@ async function getDiagnostics(workspaceUri: URI, writer: Writer): Promise { console.error(_err); diff --git a/packages/svelte-check/src/writers.ts b/packages/svelte-check/src/writers.ts index bbe1d1916..e565acd94 100644 --- a/packages/svelte-check/src/writers.ts +++ b/packages/svelte-check/src/writers.ts @@ -1,10 +1,7 @@ import * as chalk from 'chalk'; import { sep } from 'path'; -import { Writable } from "stream"; -import { - Diagnostic, - DiagnosticSeverity, -} from 'vscode-languageserver-protocol'; +import { Writable } from 'stream'; +import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; import { offsetAt } from 'svelte-language-server'; export interface Writer { @@ -15,8 +12,7 @@ export interface Writer { } export class HumanFriendlyWriter implements Writer { - constructor(private stream: Writable, private isVerbose = true) { - } + constructor(private stream: Writable, private isVerbose = true) {} start(workspaceDir: string) { if (this.isVerbose) { @@ -36,14 +32,14 @@ export class HumanFriendlyWriter implements Writer { // Display location in a format that IDEs will turn into file links const { line, character } = diagnostic.range.start; // eslint-disable-next-line max-len - this.stream.write(`${workspaceDir}${sep}${chalk.green(filename)}:${line + 1}:${character + 1}\n`); + this.stream.write( + `${workspaceDir}${sep}${chalk.green(filename)}:${line + 1}:${character + 1}\n`, + ); // Show some context around diagnostic range const startOffset = offsetAt(diagnostic.range.start, text); const endOffset = offsetAt(diagnostic.range.end, text); - const codePrev = chalk.cyan( - text.substring(Math.max(startOffset - 10, 0), startOffset) - ); + const codePrev = chalk.cyan(text.substring(Math.max(startOffset - 10, 0), startOffset)); const codeHighlight = chalk.magenta(text.substring(startOffset, endOffset)); const codePost = chalk.cyan(text.substring(endOffset, endOffset + 10)); const code = codePrev + codeHighlight + codePost; @@ -51,29 +47,41 @@ export class HumanFriendlyWriter implements Writer { if (this.isVerbose) { msg = `${diagnostic.message} ${source}\n${chalk.cyan(code)}`; - } - else { + } else { msg = `${diagnostic.message} ${source}`; } if (diagnostic.severity === DiagnosticSeverity.Error) { this.stream.write(`${chalk.red('Error')}: ${msg}\n`); - } - else { + } else { this.stream.write(`${chalk.yellow('Warn')}: ${msg}\n`); } - this.stream.write("\n"); + this.stream.write('\n'); }); } - completion(_f: number, err: number, _w: number) { + completion(_f: number, errorCount: number, warningCount: number) { this.stream.write('====================================\n'); - if (err === 0) { - this.stream.write(chalk.green(`svelte-check found no errors\n`)); + if (errorCount === 0 && warningCount === 0) { + this.stream.write(chalk.green(`svelte-check found no errors and no warnings\n`)); + } else if (errorCount === 0) { + this.stream.write( + chalk.yellow( + `svelte-check found ${warningCount} ${ + warningCount === 1 ? 'warning' : 'warnings' + }\n`, + ), + ); } else { - this.stream.write(chalk.red(`svelte-check found ${err} ${err === 1 ? 'error' : 'errors'}\n`)); + this.stream.write( + chalk.red( + `svelte-check found ${errorCount} ${ + errorCount === 1 ? 'error' : 'errors' + } and ${warningCount} ${warningCount === 1 ? 'warning' : 'warnings'}\n`, + ), + ); } } @@ -83,8 +91,7 @@ export class HumanFriendlyWriter implements Writer { } export class MachineFriendlyWriter implements Writer { - constructor(private stream: Writable) { - } + constructor(private stream: Writable) {} private log(msg: string) { this.stream.write(`${new Date().getTime()} ${msg}\n`); @@ -98,9 +105,11 @@ export class MachineFriendlyWriter implements Writer { diagnostics.forEach((d) => { const { message, severity, range } = d; const type = - severity === DiagnosticSeverity.Error ? "ERROR" : - severity === DiagnosticSeverity.Warning ? "WARNING" : - null; + severity === DiagnosticSeverity.Error + ? 'ERROR' + : severity === DiagnosticSeverity.Warning + ? 'WARNING' + : null; if (type) { const { line, character } = range.start; diff --git a/yarn.lock b/yarn.lock index b27df4c98..010899896 100644 --- a/yarn.lock +++ b/yarn.lock @@ -497,6 +497,21 @@ chokidar@3.3.0: optionalDependencies: fsevents "~2.1.1" +chokidar@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" + integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -924,6 +939,11 @@ fsevents@~2.1.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1647,6 +1667,11 @@ picomatch@^2.0.4: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== +picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -1701,6 +1726,13 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + regenerator-runtime@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz#e96bf612a3362d12bb69f7e8f74ffeab25c7ac91"