From 9fcbe20528d2960f4306b63eaef4b68267661367 Mon Sep 17 00:00:00 2001 From: matan h Date: Mon, 9 Mar 2026 10:58:25 +0200 Subject: [PATCH] refactor: isolate LSP options from lsp transport; create abstract transport --- lib/LSP/lsp.dart | 204 ++++++++++++++++++++--------- lib/LSP/lsp_socket.dart | 89 ------------- lib/LSP/lsp_stdio.dart | 231 --------------------------------- lib/LSP/lsp_transport.dart | 128 ++++++++++++++++++ lib/code_forge/controller.dart | 4 +- 5 files changed, 276 insertions(+), 380 deletions(-) delete mode 100644 lib/LSP/lsp_socket.dart delete mode 100644 lib/LSP/lsp_stdio.dart create mode 100644 lib/LSP/lsp_transport.dart diff --git a/lib/LSP/lsp.dart b/lib/LSP/lsp.dart index b96a171..870b162 100644 --- a/lib/LSP/lsp.dart +++ b/lib/LSP/lsp.dart @@ -6,10 +6,10 @@ import 'package:flutter/services.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -part 'lsp_socket.dart'; -part 'lsp_stdio.dart'; +part 'lsp_transport.dart'; -sealed class LspConfig { +// *only* the config options, not the LSP functionality itself +class LspOptions { /// The language ID of the language. /// /// languageId depends on the server you are using. @@ -31,39 +31,20 @@ sealed class LspConfig { /// Whether to disable errors from the LSP server. final bool disableError; + /// Map of user provided LSP initialization options. + final Map initializationOptions; - final StreamController> _responseController = - StreamController.broadcast(); - int _nextId = 1; - final _openDocuments = {}; - List? _serverTokenTypes; - List? _serverTokenModifiers; - - bool isInitialized = false; - - /// Stream of responses from the LSP server. - /// Use this to listen for notifications like diagnostics. - Stream> get responses => _responseController.stream; - - /// The server's semantic token types legend. - /// Returns null if not yet initialized. - List? get serverTokenTypes => _serverTokenTypes; - - /// The server's semantic token modifiers legend. - /// Returns null if not yet initialized. - List? get serverTokenModifiers => _serverTokenModifiers; - - LspConfig({ - required this.workspacePath, + LspOptions({ required this.languageId, + required this.workspacePath, + this.initializationOptions = const {}, this.capabilities = const LspClientCapabilities(), this.disableWarning = false, this.disableError = false, }); - @override bool operator ==(Object other) { - return (other is LspConfig && + return (other is LspOptions && languageId == other.languageId && workspacePath == other.workspacePath && disableError == other.disableError && @@ -73,25 +54,73 @@ sealed class LspConfig { @override int get hashCode => Object.hash(languageId, workspacePath, disableError, disableWarning); +} + +class LspConfig { + final LspTransport transport; + final LspOptions config; + + final _openDocuments = {}; + List? _serverTokenTypes; + List? _serverTokenModifiers; + + int _nextId = 1; + bool isInitialized = false; + String get languageId => config.languageId; + + LspConfig({required this.config, required this.transport}); + + /// Stream of responses from the LSP server. + /// Use this to listen for notifications like diagnostics. + Stream> get responses => transport.stream; - void dispose(); + /// The server's semantic token types legend. + /// Returns null if not yet initialized. + List? get serverTokenTypes => _serverTokenTypes; + + /// The server's semantic token modifiers legend. + /// Returns null if not yet initialized. + List? get serverTokenModifiers => _serverTokenModifiers; Future> _sendRequest({ required String method, required Map params, - }); + }) async { + final id = _nextId++; + final request = { + 'jsonrpc': '2.0', + 'id': id, + 'method': method, + 'params': params, + }; + + await transport.send(request); + + return await transport.stream.firstWhere( + (response) => response['id'] == id, + orElse: () => throw TimeoutException('No response for request $id'), + ); + } Future _sendNotification({ required String method, required Map params, - }); + }) async { + await transport.send({ + 'jsonrpc': '2.0', + 'method': method, + 'params': params, + }); + } /// This method is used to initialize the LSP server. /// /// This method is used internally by the [CodeForge] widget and calling it directly is not recommended. /// It may crash the LSP server if called multiple times. Future initialize() async { - final workspaceUri = Uri.directory(workspacePath).toString(); + final workspaceUri = Uri.directory( + config.workspacePath, + ).toString(); // note that rootUri is deprecated in favor of workspaceFolders final response = await _sendRequest( method: 'initialize', params: { @@ -100,9 +129,7 @@ sealed class LspConfig { 'workspaceFolders': [ {'uri': workspaceUri, 'name': 'workspace'}, ], - 'initializationOptions': { - 'highlight': {'enabled': true}, - }, + 'initializationOptions': config.initializationOptions, 'capabilities': _buildCapabilities(), }, ); @@ -127,12 +154,11 @@ sealed class LspConfig { await _sendNotification(method: 'initialized', params: {}); isInitialized = true; } - /// Builds the capabilities map based on enabled features. Map _buildCapabilities() { final textDocumentCapabilities = {}; - if (capabilities.codeCompletion) { + if (config.capabilities.codeCompletion) { textDocumentCapabilities['completion'] = { 'completionItem': { 'resolveSupport': { @@ -143,7 +169,7 @@ sealed class LspConfig { }; } - if (capabilities.signatureHelp) { + if (config.capabilities.signatureHelp) { textDocumentCapabilities['signatureHelp'] = { 'dynamicRegistration': false, 'signatureInformation': { @@ -155,13 +181,13 @@ sealed class LspConfig { }; } - if (capabilities.hoverInfo) { + if (config.capabilities.hoverInfo) { textDocumentCapabilities['hover'] = { 'contentFormat': ['markdown'], }; } - if (capabilities.semanticHighlighting) { + if (config.capabilities.semanticHighlighting) { textDocumentCapabilities['semanticTokens'] = { 'dynamicRegistration': false, 'tokenTypes': sematicMap['tokenTypes'], @@ -174,21 +200,21 @@ sealed class LspConfig { }; } - if (capabilities.inlayHint) { + if (config.capabilities.inlayHint) { textDocumentCapabilities['inlayHint'] = {'dynamicRegistration': false}; } - if (capabilities.documentColor) { + if (config.capabilities.documentColor) { textDocumentCapabilities['colorProvider'] = { 'dynamicRegistration': false, }; } - if (capabilities.codeFolding) { + if (config.capabilities.codeFolding) { textDocumentCapabilities['foldingRange'] = {'dynamicRegistration': false}; } - if (capabilities.documentHighlight) { + if (config.capabilities.documentHighlight) { textDocumentCapabilities['documentHighlight'] = { 'dynamicRegistration': false, }; @@ -230,7 +256,7 @@ sealed class LspConfig { params: { 'textDocument': { 'uri': Uri.file(filePath).toString(), - 'languageId': languageId, + 'languageId': config.languageId, 'version': version, 'text': text, }, @@ -315,7 +341,7 @@ sealed class LspConfig { int line, int character, ) async { - if (!capabilities.codeCompletion) return []; + if (!config.capabilities.codeCompletion) return []; List completion = []; final response = await _sendRequest( method: 'textDocument/completion', @@ -365,7 +391,7 @@ sealed class LspConfig { /// This method is used internally by the [CodeForge], calling this with appropriate parameters will returns a [String]. /// If the LSP server does not support hover or the location provided is invalid, it will return an empty string. Future getHover(String filePath, int line, int character) async { - if (!capabilities.hoverInfo) return ''; + if (!config.capabilities.hoverInfo) return ''; final response = await _sendRequest( method: 'textDocument/hover', params: _commonParams(filePath, line, character), @@ -435,7 +461,7 @@ sealed class LspConfig { String? triggerCharacter, bool isRetrigger = false, }) async { - if (!capabilities.signatureHelp) { + if (!config.capabilities.signatureHelp) { return LspSignatureHelps( activeParameter: -1, activeSignature: -1, @@ -526,7 +552,7 @@ sealed class LspConfig { int line, int character, ) async { - if (!capabilities.documentHighlight) return []; + if (!config.capabilities.documentHighlight) return []; final response = await _sendRequest( method: 'textDocument/documentHighlight', params: _commonParams(filePath, line, character), @@ -545,7 +571,7 @@ sealed class LspConfig { required double alpha, required Map range, }) async { - if (!capabilities.documentColor) return {'result': []}; + if (!config.capabilities.documentColor) return {'result': []}; /// Requests color presentation(s) for a color at a given range. /// @@ -575,7 +601,7 @@ sealed class LspConfig { /// the raw server response as a map; callers should read `response['result']` /// to obtain the list of color entries. Future> getDocumentColor(String filePath) async { - if (!capabilities.documentColor) return {'result': []}; + if (!config.capabilities.documentColor) return {'result': []}; final response = await _sendRequest( method: "textDocument/documentColor", params: { @@ -592,7 +618,7 @@ sealed class LspConfig { /// folded in the editor. This method returns the raw server response as a /// map; examine `response['result']` for the folding range list. Future> getLSPFoldRanges(String filePath) async { - if (!capabilities.codeFolding) return {'result': []}; + if (!config.capabilities.codeFolding) return {'result': []}; final response = await _sendRequest( method: "textDocument/foldingRange", params: { @@ -617,7 +643,7 @@ sealed class LspConfig { int endLine, int endCharacter, ) async { - if (!capabilities.inlayHint) return {'result': []}; + if (!config.capabilities.inlayHint) return {'result': []}; final response = await _sendRequest( method: "textDocument/inlayHint", params: { @@ -639,7 +665,7 @@ sealed class LspConfig { int line, int character, ) async { - if (!capabilities.goToDefinition) return {}; + if (!config.capabilities.goToDefinition) return {}; final response = await _sendRequest( method: 'textDocument/definition', params: _commonParams(filePath, line, character), @@ -783,7 +809,7 @@ sealed class LspConfig { int character, String newName, ) async { - if (!capabilities.rename) return {}; + if (!config.capabilities.rename) return {}; final response = await _sendRequest( method: 'textDocument/rename', params: {..._commonParams(filePath, line, character), 'newName': newName}, @@ -800,7 +826,7 @@ sealed class LspConfig { int line, int character, ) async { - if (!capabilities.rename) return null; + if (!config.capabilities.rename) return null; final response = await _sendRequest( method: 'textDocument/prepareRename', params: _commonParams(filePath, line, character), @@ -820,7 +846,7 @@ sealed class LspConfig { required int endCharacter, List> diagnostics = const [], }) async { - if (!capabilities.codeAction) return []; + if (!config.capabilities.codeAction) return []; final response = await _sendRequest( method: 'textDocument/codeAction', params: { @@ -976,7 +1002,7 @@ sealed class LspConfig { /// /// Returns a list of [LspSemanticToken] objects representing syntax tokens for highlighting. Future> getSemanticTokensFull(String filePath) async { - if (!capabilities.semanticHighlighting) return []; + if (!config.capabilities.semanticHighlighting) return []; final response = await _sendRequest( method: 'textDocument/semanticTokens/full', params: { @@ -1022,6 +1048,68 @@ sealed class LspConfig { return result; } + +} +/// LspStdioConfig short for IO connect +class LspStdioConfig extends LspConfig { + LspStdioConfig._(LspOptions config, StdioTransport transport) + : super(config: config, transport: transport); + + static Future start({ + required String executable, + required String workspacePath, + required String languageId, + LspClientCapabilities capabilities = const LspClientCapabilities(), + List? args, + Map? environment, + bool disableWarning = false, + bool disableError = false, + }) async { + final process = await Process.start( + executable, + args ?? [], + environment: environment, + ); + + final transport = StdioTransport(process); + final config = LspOptions( + workspacePath: workspacePath, + languageId: languageId, + capabilities: capabilities, + disableWarning: disableWarning, + disableError: disableError, + ); + + final client = LspStdioConfig._(config, transport); + await client.initialize(); + return client; + } + + // calling .lspConfig still works + LspConfig get lspConfig => this; +} + +/// LspSocketConfig short for ws connect +class LspSocketConfig extends LspConfig { + LspSocketConfig({ + required String workspacePath, + required String languageId, + required String serverUrl, + LspClientCapabilities capabilities = const LspClientCapabilities(), + bool disableWarning = false, + bool disableError = false, + }) : super( + config: LspOptions( + workspacePath: workspacePath, + languageId: languageId, + capabilities: capabilities, + disableWarning: disableWarning, + disableError: disableError, + ), + transport: SocketTransport(WebSocketChannel.connect(Uri.parse(serverUrl))), + ); + + Future connect() async => await initialize(); } enum CompletionItemType { diff --git a/lib/LSP/lsp_socket.dart b/lib/LSP/lsp_socket.dart deleted file mode 100644 index ea09cc2..0000000 --- a/lib/LSP/lsp_socket.dart +++ /dev/null @@ -1,89 +0,0 @@ -part of 'lsp.dart'; - -/// A configuration class for Language Server Protocol (LSP) using WebSocket communication. -/// -/// Documenation available [here](https://github.com/heckmon/flutter_code_crafter/blob/main/docs/LSPClient.md). -/// -///Example: -/// create a [LspSocketConfig] object and pass it to the [CodeForge] widget. -/// -///```dart -///final lspConfig = LspSocketConfig( -/// workspacePath: "/home/athul/Projects/lsp", -/// languageId: "python", -/// serverUrl: "ws://localhost:5656" -///), -///``` -///Then pass the `lspConfig` instance to the `CodeForge` widget: -/// -///```dart -///CodeForge( -/// controller: controller, -/// theme: anOldHopeTheme, -/// lspConfig: lspConfig, // Pass the LSP config here -///), -///``` -class LspSocketConfig extends LspConfig { - /// The URL of the LSP server to connect to via WebSocket. - final String serverUrl; - final WebSocketChannel _channel; - - LspSocketConfig({ - required super.workspacePath, - required super.languageId, - required this.serverUrl, - super.capabilities, - super.disableWarning, - super.disableError, - }) : _channel = WebSocketChannel.connect(Uri.parse(serverUrl)); - - /// This method is used to initialize the LSP server. and it's used internally by the [CodeCrafter] widget. - /// Calling it directly is not recommended and may crash the LSP server if called multiple times. - Future connect() async { - _channel.stream.listen((data) { - try { - final json = jsonDecode(data as String); - _responseController.add(json); - } catch (e) { - throw FormatException('Invalid JSON response: $data', e); - } - }); - } - - @override - Future> _sendRequest({ - required String method, - required Map params, - }) async { - final id = _nextId++; - final request = { - 'jsonrpc': '2.0', - 'id': id, - 'method': method, - 'params': params, - }; - - _channel.sink.add(jsonEncode(request)); - - return await _responseController.stream.firstWhere( - (response) => response['id'] == id, - orElse: () => throw TimeoutException('No response for request $id'), - ); - } - - @override - Future _sendNotification({ - required String method, - required Map params, - }) async { - _channel.sink.add( - jsonEncode({'jsonrpc': '2.0', 'method': method, 'params': params}), - ); - } - - @override - void dispose() { - _channel.sink.close(); - _responseController.close(); - } -} diff --git a/lib/LSP/lsp_stdio.dart b/lib/LSP/lsp_stdio.dart deleted file mode 100644 index 85091e7..0000000 --- a/lib/LSP/lsp_stdio.dart +++ /dev/null @@ -1,231 +0,0 @@ -part of 'lsp.dart'; - -/// This class provides a configuration for Language Server Protocol (LSP) using standard input/output communication. -/// Little bit complex compared to [LspSocketConfig]. -/// -/// /// Documenation available [here](https://github.com/heckmon/flutter_code_crafter/blob/main/docs/LSPClient.md). -/// -/// Example: -/// -/// Create an async method to initialize the LSP configuration. -///```dart -///Future _initLsp() async { -/// try { -/// final config = await LspStdioConfig.start( -/// executable: '/home/athul/.nvm/versions/node/v20.19.2/bin/pyright-langserver', -/// args: ['--stdio'] -/// workspacePath: '/home/athul/Projects/lsp', -/// languageId: 'python', -/// ); -/// -/// return config; -/// } catch (e) { -/// debugPrint('LSP Initialization failed: $e'); -/// return null; -/// } -/// } -/// ``` -/// Then use a `FutureBuilder` to initialize the LSP configuration and pass it to the `CodeForge` widget: -///```dart -/// @override -/// Widget build(BuildContext context) { -/// return MaterialApp( -/// home: Scaffold( -/// body: SafeArea( -/// child: FutureBuilder( -/// future: _initLsp(), // Call the async method to get the LSP config -/// builder: (context, snapshot) { -/// if(snapshot.connectionState == ConnectionState.waiting) { -/// return Center(child: CircularProgressIndicator()); -/// } -/// return CodeForge( -/// wrapLines: true, -/// editorTheme: anOldHopeTheme, -/// controller: controller, -/// filePath: '/home/athul/Projects/lsp/example.py', -/// textStyle: TextStyle(fontSize: 15, fontFamily: 'monospace'), -/// lspConfig: snapshot.data, // Pass the LSP config here -/// ); -/// } -/// ), -/// ) -/// ), -/// ); -/// } -class LspStdioConfig extends LspConfig { - /// location of the LSP executable, such as `pyright-langserver`, `rust-analyzer`, etc. - /// - /// To get the `executable` path, you can use the `which` command in the terminal. For example, to get the path of the `pyright-langserver`, you can use the following command: - /// - ///```bash - ///which pyright-langserver - ///``` - final String executable; - - /// Optional arguments for the executable. - final List? args; - - /// Optional environement variables for the executable. - final Map? environment; - - late Process _process; - final _buffer = []; - bool _isSending = false; - - LspStdioConfig._({ - required this.executable, - required super.workspacePath, - required super.languageId, - this.args, - this.environment, - super.capabilities, - super.disableWarning, - super.disableError, - }); - - static Future start({ - required String executable, - required String workspacePath, - required String languageId, - LspClientCapabilities capabilities = const LspClientCapabilities(), - List? args, - Map? environment, - bool disableWarning = false, - bool disableError = false, - }) async { - final config = LspStdioConfig._( - executable: executable, - languageId: languageId, - workspacePath: workspacePath, - args: args, - environment: environment, - disableWarning: disableWarning, - disableError: disableError, - capabilities: capabilities, - ); - await config._startProcess(); - return config; - } - - Future _startProcess() async { - _process = await Process.start( - executable, - args ?? [], - environment: environment, - ); - _process.stdout.listen(_handleStdoutData); - _process.stderr.listen((data) => debugPrint(utf8.decode(data))); - } - - int get pid => _process.pid; - Future get exitCode => _process.exitCode; - Process get process => _process; - - void _handleStdoutData(List data) { - _buffer.addAll(data); - while (_buffer.isNotEmpty) { - final headerEnd = _findHeaderEnd(); - if (headerEnd == -1) return; - final header = utf8.decode(_buffer.sublist(0, headerEnd)); - final contentLength = int.parse( - RegExp(r'Content-Length: (\d+)').firstMatch(header)?.group(1) ?? '0', - ); - if (_buffer.length < headerEnd + 4 + contentLength) return; - final messageStart = headerEnd + 4; - final messageEnd = messageStart + contentLength; - final messageBytes = _buffer.sublist(messageStart, messageEnd); - _buffer.removeRange(0, messageEnd); - try { - final json = jsonDecode(utf8.decode(messageBytes)); - _responseController.add(json); - } catch (e) { - throw FormatException( - 'Invalid JSON message $e', - utf8.decode(messageBytes), - ); - } - } - } - - int _findHeaderEnd() { - final endSequence = [13, 10, 13, 10]; - for (var i = 0; i <= _buffer.length - endSequence.length; i++) { - if (List.generate( - endSequence.length, - (j) => _buffer[i + j], - ).every((byte) => endSequence.contains(byte))) { - return i; - } - } - return -1; - } - - @override - Future> _sendRequest({ - required String method, - required Map params, - }) async { - final id = _nextId++; - final request = { - 'jsonrpc': '2.0', - 'id': id, - 'method': method, - 'params': params, - }; - await _sendLspMessage(request); - - return await _responseController.stream.firstWhere( - (response) => response['id'] == id, - orElse: () => throw TimeoutException('No response for request $id'), - ); - } - - @override - Future _sendNotification({ - required String method, - required Map params, - }) async { - await _sendLspMessage({ - 'jsonrpc': '2.0', - 'method': method, - 'params': params, - }); - } - - Future _sendLspMessage(Map message) async { - final completer = Completer(); - Future sendOperation() async { - try { - final body = utf8.encode(jsonEncode(message)); - final header = utf8.encode('Content-Length: ${body.length}\r\n\r\n'); - final combined = [...header, ...body]; - _process.stdin.add(combined); - await _process.stdin.flush(); - completer.complete(); - } catch (e) { - completer.completeError(e); - } - } - - if (!_isSending) { - _isSending = true; - await sendOperation(); - _isSending = false; - } else { - while (_isSending) { - await Future.delayed(const Duration(microseconds: 100)); - } - _isSending = true; - await sendOperation(); - _isSending = false; - } - - return completer.future; - } - - @override - void dispose() { - _process.kill(); - _responseController.close(); - } -} diff --git a/lib/LSP/lsp_transport.dart b/lib/LSP/lsp_transport.dart new file mode 100644 index 0000000..eb9409a --- /dev/null +++ b/lib/LSP/lsp_transport.dart @@ -0,0 +1,128 @@ +part of 'lsp.dart'; + +/// minimal requirements for LSP transport mode +abstract class LspTransport { + Stream> get stream; + Future send(Map message); + void dispose(); +} + +/// process content-length and I/O of a process +class StdioTransport extends LspTransport { + final Process _process; + final StreamController> _responseController = + StreamController.broadcast(); + final List _buffer = []; + bool _isSending = false; + + StdioTransport(this._process) { + _process.stdout.listen(_handleStdoutData); + _process.stderr.listen((data) => debugPrint(utf8.decode(data))); + } + void _handleStdoutData(List data) { + _buffer.addAll(data); + while (_buffer.isNotEmpty) { + final headerEnd = _findHeaderEnd(); + if (headerEnd == -1) return; + final header = utf8.decode(_buffer.sublist(0, headerEnd)); + final contentLength = int.parse( + RegExp(r'Content-Length: (\d+)').firstMatch(header)?.group(1) ?? '0', + ); + if (_buffer.length < headerEnd + 4 + contentLength) return; + final messageStart = headerEnd + 4; + final messageEnd = messageStart + contentLength; + final messageBytes = _buffer.sublist(messageStart, messageEnd); + _buffer.removeRange(0, messageEnd); + try { + final json = jsonDecode(utf8.decode(messageBytes)); + _responseController.add(json); + } catch (e) { + throw FormatException( + 'Invalid JSON message $e', + utf8.decode(messageBytes), + ); + } + } + } + @override + Future send(Map message) async { + final completer = Completer(); + Future sendOperation() async { + try { + final body = utf8.encode(jsonEncode(message)); + final header = utf8.encode('Content-Length: ${body.length}\r\n\r\n'); + final combined = [...header, ...body]; + _process.stdin.add(combined); + await _process.stdin.flush(); + completer.complete(); + } catch (e) { + completer.completeError(e); + } + } + + if (!_isSending) { + _isSending = true; + await sendOperation(); + _isSending = false; + } else { + while (_isSending) { + await Future.delayed(const Duration(microseconds: 100)); + } + _isSending = true; + await sendOperation(); + _isSending = false; + } + + return completer.future; + } + + int _findHeaderEnd() { + final endSequence = [13, 10, 13, 10]; + for (var i = 0; i <= _buffer.length - endSequence.length; i++) { + if (List.generate( + endSequence.length, + (j) => _buffer[i + j], + ).every((byte) => endSequence.contains(byte))) { + return i; + } + } + return -1; + } + + @override + Stream> get stream => _responseController.stream; + + @override + void dispose() { + _process.kill(); + _responseController.close(); + } + + +} + +// simple transport mode for socket +class SocketTransport extends LspTransport { + final WebSocketChannel _channel; + final StreamController> _controller = + StreamController.broadcast(); + + SocketTransport(this._channel) { + _channel.stream.listen((data) { + _controller.add(jsonDecode(data as String)); + }); + } + + @override + Stream> get stream => _controller.stream; + + @override + Future send(Map message) async => + _channel.sink.add(jsonEncode(message)); + + @override + void dispose() { + _channel.sink.close(); + _controller.close(); + } +} diff --git a/lib/code_forge/controller.dart b/lib/code_forge/controller.dart index f13276e..b5bffd2 100644 --- a/lib/code_forge/controller.dart +++ b/lib/code_forge/controller.dart @@ -117,10 +117,10 @@ class CodeForgeController implements DeltaTextInputClient { for (final item in rawDiagnostics) { if (item is! Map) continue; int severity = item['severity'] ?? 0; - if (severity == 1 && lspConfig!.disableError) { + if (severity == 1 && lspConfig!.config.disableError) { severity = 0; } - if (severity == 2 && lspConfig!.disableWarning) { + if (severity == 2 && lspConfig!.config.disableWarning) { severity = 0; } if (severity > 0) {