diff --git a/client/extension.ts b/client/extension.ts index 02a27fa..6cba5e8 100644 --- a/client/extension.ts +++ b/client/extension.ts @@ -87,7 +87,9 @@ export async function activate(context: ExtensionContext) { logger.debug('[client] onDidChangeActiveTextEditor', textDocument?.uri.fsPath); - const isEnabled = workspace.getConfiguration('xo').get('enable', true); + const isEnabled = workspace + .getConfiguration('xo', textDocument) + .get('enable', true); if (!isEnabled) { logger.debug('[client] onDidChangeActiveTextEditor > XO is not enabled'); diff --git a/server/get-document-config.ts b/server/get-document-config.ts index 459be04..5e89720 100644 --- a/server/get-document-config.ts +++ b/server/get-document-config.ts @@ -11,22 +11,21 @@ async function getDocumentConfig( this: LintServer, document: TextDocumentIdentifier ): Promise { - const folderUri = path.dirname(document.uri); - - if (this.configurationCache.has(folderUri)) { - const config: XoConfig = this.configurationCache.get(folderUri)!; + if (this.configurationCache.has(document.uri)) { + const config: XoConfig = this.configurationCache.get(document.uri)!; if (config !== undefined) return config; return {}; } - const config: XoConfig = (await this.connection.workspace.getConfiguration({ - scopeUri: folderUri, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const config: XoConfig = await this.connection.workspace.getConfiguration({ + scopeUri: document.uri, section: 'xo' - })) as XoConfig; + }); - this.configurationCache.set(folderUri, config); + this.configurationCache.set(document.uri, config); return config; } diff --git a/server/lint-document.ts b/server/lint-document.ts index 14e7193..8294f48 100644 --- a/server/lint-document.ts +++ b/server/lint-document.ts @@ -17,7 +17,9 @@ export async function lintDocument(this: LintServer, document: TextDocument): Pr if (document.version !== currentDocument.version) return; - const {overrideSeverity} = await this.getDocumentConfig(document); + const {overrideSeverity, enable} = await this.getDocumentConfig(document); + + if (!enable) return; const {results, rulesMeta} = await this.getLintResults(document); diff --git a/server/server.ts b/server/server.ts index 974984e..43f995b 100644 --- a/server/server.ts +++ b/server/server.ts @@ -57,7 +57,6 @@ class LintServer { documentFixCache: Map>; hasShownResolutionError: boolean; hasReceivedShutdownRequest?: boolean; - debounceTime = 0; constructor({isTest}: {isTest?: boolean} = {}) { /** @@ -157,7 +156,7 @@ class LintServer { this.xoCache = new Map(); /** - * A mapping of folderPaths to configuration options + * A mapping of document uri strings to configuration options */ this.configurationCache = new Map(); @@ -217,13 +216,6 @@ class LintServer { * Handle connection.onDidChangeConfiguration */ async handleDidChangeConfiguration(params: ChangeConfigurationParams) { - if ( - Number.isInteger(Number(params?.settings?.xo?.debounce)) && - Number(params?.settings?.xo?.debounce) !== this.debounceTime - ) { - this.debounceTime = params.settings.xo.debounce ?? 0; - } - // recache each folder config this.configurationCache.clear(); return this.lintDocuments(this.documents.all()); @@ -286,7 +278,7 @@ class LintServer { const config = await this.getDocumentConfig(params.textDocument); - if (config === undefined || !config?.format?.enable) { + if (config === undefined || !config?.format?.enable || !config.enable) { resolve([]); return; } @@ -345,7 +337,18 @@ class LintServer { } const document = this.documents.get(params.textDocument.uri); - if (!document) return; + + if (!document) { + resolve(undefined); + return; + } + + const config = await this.getDocumentConfig(document); + + if (config === undefined || !config.enable) { + resolve(undefined); + return; + } const codeActions: CodeAction[] = []; if (context.only?.includes(CodeActionKind.SourceFixAll)) { @@ -397,8 +400,9 @@ class LintServer { return; } + const config = await this.getDocumentConfig(event.document); const {default: debounce} = await import('p-debounce'); - await debounce(this.lintDocument, this.debounceTime)(event.document); + await debounce(this.lintDocument, config.debounce ?? 0)(event.document); } catch (error: unknown) { if (error instanceof Error) this.logError(error); } diff --git a/test/index.ts b/test/index.ts index 0c9c7d2..7beda3c 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,9 +1,14 @@ /* eslint-disable import/no-unassigned-import */ // since globs are not fully supported in node v18 and v20 we import the files manually here - +import process from 'node:process'; // TODO: remove this file once node v21 is LTS import './server.test.js'; import './lsp/document-sync.test.js'; import './lsp/initialization.test.js'; import './lsp/code-actions.test.js'; import './code-actions-builder.test.js'; + +process.on('unhandledRejection', (error) => { + console.error(error); + process.exit(1); +}); diff --git a/test/lsp/code-actions.test.ts b/test/lsp/code-actions.test.ts index bd08100..2d9fc09 100644 --- a/test/lsp/code-actions.test.ts +++ b/test/lsp/code-actions.test.ts @@ -9,6 +9,7 @@ import { type CodeActionParams, type Range, type TextDocumentIdentifier + // type Connection } from 'vscode-languageserver'; import Server from '../../server/server.js'; import { @@ -27,6 +28,7 @@ describe('Server code actions', async () => { log: Mock; getDocumentFormatting: Mock; documents: Map & {all?: typeof Map.prototype.values}; + getDocumentConfig: Mock; }; test.beforeEach((t) => { @@ -42,6 +44,7 @@ describe('Server code actions', async () => { server.documents = documents; mock.method(server, 'log', noop); mock.method(server, 'getDocumentFormatting'); + mock.method(server, 'getDocumentConfig', async () => ({enable: true})); }); test.afterEach(async () => { @@ -64,6 +67,7 @@ describe('Server code actions', async () => { context: {diagnostics: [Diagnostic.create(range, 'test message', 1, 'test', 'test')]} }; const codeActions = await server.handleCodeActionRequest(mockCodeActionParams); + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.deepEqual(codeActions, []); }); @@ -79,10 +83,11 @@ describe('Server code actions', async () => { } }; const codeActions = await server.handleCodeActionRequest(mockCodeActionParams); - + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.deepEqual(codeActions, [ {title: 'Fix all XO auto-fixable problems', kind: 'source.fixAll', edit: {changes: {uri: []}}} ]); + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.equal(server.getDocumentFormatting.mock.callCount(), 1); assert.deepEqual(server.getDocumentFormatting.mock.calls[0].arguments, ['uri']); }); @@ -99,7 +104,7 @@ describe('Server code actions', async () => { } }; const codeActions = await server.handleCodeActionRequest(mockCodeActionParams); - + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.deepEqual(codeActions, []); assert.equal(server.getDocumentFormatting.mock.callCount(), 0); }); @@ -115,7 +120,7 @@ describe('Server code actions', async () => { } }; const codeActions = await server.handleCodeActionRequest(mockCodeActionParams); - + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.deepEqual(codeActions, []); assert.equal(server.getDocumentFormatting.mock.callCount(), 0); }); @@ -141,4 +146,13 @@ describe('Server code actions', async () => { getIgnoreFileCodeAction() ]); }); + + await test('codeAction with only quickfix produces quickfix code actions', async (t) => { + const params = getCodeActionParams(); + params.context.only = [CodeActionKind.QuickFix]; + mock.method(server, 'getDocumentConfig', async () => ({enable: false})); + const codeActions = await server.handleCodeActionRequest(params); + + assert.deepStrictEqual(codeActions, undefined); + }); }); diff --git a/test/lsp/document-sync.test.ts b/test/lsp/document-sync.test.ts index a9df607..92dbc43 100644 --- a/test/lsp/document-sync.test.ts +++ b/test/lsp/document-sync.test.ts @@ -8,10 +8,14 @@ import Server from '../../server/server.js'; const noop = () => {}; describe('Server documents syncing', () => { - let server: Omit & { + let server: Omit< + Server, + 'lintDocument' | 'documents' | 'connection' | 'log' | 'getDocumentConfig' + > & { lintDocument: Mock; log: Mock; documents: Map & {all?: typeof Map.prototype.values}; + getDocumentConfig: Mock; connection: Omit & { sendDiagnostics: Mock; }; @@ -30,6 +34,7 @@ describe('Server documents syncing', () => { server.documents = documents; mock.method(server, 'log', noop); mock.method(server, 'lintDocument', noop); + mock.method(server, 'getDocumentConfig', async () => ({enable: true})); mock.method(server.connection, 'sendDiagnostics', noop); }); @@ -53,6 +58,7 @@ describe('Server documents syncing', () => { resolve(undefined); }); }); + assert.equal(server.getDocumentConfig.mock.callCount(), 1); assert.equal(server.lintDocument.mock.callCount(), 1); }); @@ -92,4 +98,18 @@ describe('Server documents syncing', () => { diagnostics: [] }); }); + + test('Server.handleDocumentOnDidChangeContent does not send diagnostics if xo is disabled', async (t) => { + mock.method(server, 'getDocumentConfig', async () => ({enable: false})); + server.handleDocumentsOnDidChangeContent({ + document: TextDocument.create('uri', 'javascript', 1, 'content') + }); + await new Promise((resolve) => { + server.queue.once('end', () => { + resolve(undefined); + }); + }); + assert.equal(server.getDocumentConfig.mock.callCount(), 1); + assert.equal(server.connection.sendDiagnostics.mock.callCount(), 0); + }); });