diff --git a/extras/plugin_example/README.md b/extras/plugin_example/README.md index d7d12cd4..8b3bed47 100644 --- a/extras/plugin_example/README.md +++ b/extras/plugin_example/README.md @@ -6,7 +6,7 @@ plugin. To set up the plugin, run the following steps 1. Change the file `moor/tools/analyzer_plugin/pubspec.yaml` so that the `dependency_overrides` section points to the location where you cloned this repository. This is needed because we - can't use relative paths for dependencies in analyzer plugins yet. + can't use relative paths for dependencies in analyzer plugins yet- see https://dartbug.com/35281 2. In VS Code, change `dart.additionalAnalyzerFileExtensions` to include `moor` files: ```json { @@ -15,10 +15,14 @@ plugin. To set up the plugin, run the following steps ] } ``` + To diagnose errors with the plugin, turning on the diagnostics server by setting a + `dart.analyzerDiagnosticsPort` and enabling the instrumentation log via `dart.analyzerInstrumentationLogFile` + is recommended as well. 3. If you already had the project open, close and re-open VS Code. Otherwise, simply open this project. -4. Type around in a `.moor` file - notice how you still don't get syntax highlighting because - VS Code required a static grammar for files and can't use a language server for that :( +4. Type around in a `.moor` file. +5. Notice how you don't see anything (https://github.com/Dart-Code/Dart-Code/issues/1981), but + at least the plugin output appears in the instrumentation log. Debugging plugins is not fun. See the [docs](https://github.com/dart-lang/sdk/blob/master/pkg/analyzer_plugin/doc/tutorial/debugging.md) on some general guidance, and good luck. Enabling the analyzer diagnostics server can help. diff --git a/moor_generator/lib/src/analyzer/session.dart b/moor_generator/lib/src/analyzer/session.dart index 9aedfd14..d9f4495c 100644 --- a/moor_generator/lib/src/analyzer/session.dart +++ b/moor_generator/lib/src/analyzer/session.dart @@ -42,6 +42,8 @@ abstract class FileTask { final ErrorSink errors = ErrorSink(); + String get path => backendTask.entrypoint.path; + FileTask(this.backendTask, this.session); void reportError(MoorError error) => errors.report(error); diff --git a/moor_generator/lib/src/backends/plugin/plugin.dart b/moor_generator/lib/src/backends/plugin/plugin.dart index fb151bd7..73c29c28 100644 --- a/moor_generator/lib/src/backends/plugin/plugin.dart +++ b/moor_generator/lib/src/backends/plugin/plugin.dart @@ -1,17 +1,20 @@ import 'package:analyzer/src/context/context_root.dart'; // ignore: implementation_imports import 'package:analyzer/src/context/builder.dart'; // ignore: implementation_imports import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer_plugin/plugin/assist_mixin.dart'; import 'package:analyzer_plugin/plugin/completion_mixin.dart'; import 'package:analyzer_plugin/plugin/folding_mixin.dart'; import 'package:analyzer_plugin/plugin/highlights_mixin.dart'; import 'package:analyzer_plugin/plugin/outline_mixin.dart'; import 'package:analyzer_plugin/plugin/plugin.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; import 'package:analyzer_plugin/utilities/completion/completion_core.dart'; import 'package:analyzer_plugin/utilities/folding/folding.dart'; import 'package:analyzer_plugin/utilities/highlights/highlights.dart'; import 'package:analyzer_plugin/utilities/outline/outline.dart'; import 'package:moor_generator/src/backends/plugin/backend/file_tracker.dart'; +import 'package:moor_generator/src/backends/plugin/services/assists/assist_service.dart'; import 'package:moor_generator/src/backends/plugin/services/autocomplete.dart'; import 'package:moor_generator/src/backends/plugin/services/errors.dart'; import 'package:moor_generator/src/backends/plugin/services/folding.dart'; @@ -23,7 +26,12 @@ import 'backend/driver.dart'; import 'backend/logger.dart'; class MoorPlugin extends ServerPlugin - with OutlineMixin, HighlightsMixin, FoldingMixin, CompletionMixin { + with + OutlineMixin, + HighlightsMixin, + FoldingMixin, + CompletionMixin, + AssistsMixin { MoorPlugin(ResourceProvider provider) : super(provider) { setupLogger(this); } @@ -118,7 +126,7 @@ class MoorPlugin extends ServerPlugin @override List getCompletionContributors(String path) { - return [const MoorCompletingContributor()]; + return const [MoorCompletingContributor()]; } @override @@ -130,4 +138,20 @@ class MoorPlugin extends ServerPlugin return MoorCompletionRequest(parameters.offset, resourceProvider, task); } + + @override + List getAssistContributors(String path) { + return const [AssistService()]; + } + + @override + Future getAssistRequest( + plugin.EditGetAssistsParams parameters) async { + final path = parameters.file; + final driver = _moorDriverForPath(path); + final task = await driver.parseMoorFile(path); + + return MoorAssistRequest( + task, parameters.length, parameters.offset, resourceProvider); + } } diff --git a/moor_generator/lib/src/backends/plugin/services/assists/assist_service.dart b/moor_generator/lib/src/backends/plugin/services/assists/assist_service.dart new file mode 100644 index 00000000..bcd215fa --- /dev/null +++ b/moor_generator/lib/src/backends/plugin/services/assists/assist_service.dart @@ -0,0 +1,47 @@ +import 'package:analyzer_plugin/protocol/protocol_common.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; +import 'package:moor_generator/src/backends/plugin/services/requests.dart'; +import 'package:sqlparser/sqlparser.dart'; + +part 'column_nullability.dart'; + +class AssistService implements AssistContributor { + const AssistService(); + + final _nullability = const ColumnNullability(); + + @override + void computeAssists(AssistRequest request, AssistCollector collector) { + final moorRequest = request as MoorAssistRequest; + final parseResult = moorRequest.task.lastResult.parseResult; + final relevantNodes = + parseResult.findNodesAtPosition(request.offset, length: request.length); + + for (var node in relevantNodes.expand((node) => node.selfAndParents)) { + _handleNode(collector, node, moorRequest.task.path); + } + } + + void _handleNode(AssistCollector collector, AstNode node, String path) { + if (node is ColumnDefinition) { + _nullability.contribute(collector, node, path); + } + } +} + +abstract class _AssistOnNodeContributor { + const _AssistOnNodeContributor(); + + void contribute(AssistCollector collector, T node, String path); +} + +class AssistId { + final String id; + final int priority; + + const AssistId._(this.id, this.priority); + + static const makeNullable = AssistId._('make_column_nullable', 100); + static const makeNotNull = AssistId._('make_column_not_nullable', 10); +} diff --git a/moor_generator/lib/src/backends/plugin/services/assists/column_nullability.dart b/moor_generator/lib/src/backends/plugin/services/assists/column_nullability.dart new file mode 100644 index 00000000..bad5ed6a --- /dev/null +++ b/moor_generator/lib/src/backends/plugin/services/assists/column_nullability.dart @@ -0,0 +1,44 @@ +part of 'assist_service.dart'; + +class ColumnNullability extends _AssistOnNodeContributor { + const ColumnNullability(); + + @override + void contribute( + AssistCollector collector, ColumnDefinition node, String path) { + final notNull = node.findConstraint(); + + if (notNull == null) { + // there is no not-null constraint on this column, suggest to add one at + // the end of the definition + final end = node.lastPosition; + final id = AssistId.makeNotNull; + + collector.addAssist(PrioritizedSourceChange( + id.priority, + SourceChange('Add a NOT NULL constraint', id: id.id, edits: [ + SourceFileEdit( + path, + -1, + edits: [ + SourceEdit(end, 0, ' NOT NULL'), + ], + ) + ]), + )); + } else { + // suggest to remove the NOT NULL constraint, e.g. to make this column + // nullable + final id = AssistId.makeNullable; + + collector.addAssist(PrioritizedSourceChange( + id.priority, + SourceChange('Make this column nullable', id: id.id, edits: [ + SourceFileEdit(path, -1, edits: [ + SourceEdit(notNull.firstPosition, notNull.lastPosition, '') + ]) + ]), + )); + } + } +} diff --git a/moor_generator/lib/src/backends/plugin/services/requests.dart b/moor_generator/lib/src/backends/plugin/services/requests.dart index 767106d8..9a9d2c5b 100644 --- a/moor_generator/lib/src/backends/plugin/services/requests.dart +++ b/moor_generator/lib/src/backends/plugin/services/requests.dart @@ -1,4 +1,5 @@ import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer_plugin/utilities/assist/assist.dart'; import 'package:analyzer_plugin/utilities/completion/completion_core.dart'; import 'package:analyzer_plugin/utilities/folding/folding.dart'; import 'package:analyzer_plugin/utilities/highlights/highlights.dart'; @@ -32,3 +33,18 @@ class MoorCompletionRequest extends CompletionRequest { MoorCompletionRequest(this.offset, this.resourceProvider, this.task); } + +class MoorAssistRequest extends AssistRequest { + final MoorTask task; + + @override + final int length; + + @override + final int offset; + + @override + final ResourceProvider resourceProvider; + + MoorAssistRequest(this.task, this.length, this.offset, this.resourceProvider); +} diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 2734145b..003f41ac 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -56,7 +56,10 @@ abstract class AstNode { /// The last position that belongs to node, exclusive. Not set for all nodes. int get lastPosition => last.span.end.offset; - FileSpan get span => first.span.expand(last.span); + FileSpan get span { + if (first == null || last == null) return null; + return first.span.expand(last.span); + } /// Sets the [AstNode.first] and [AstNode.last] property in one go. void setSpan(Token first, Token last) { @@ -74,6 +77,12 @@ abstract class AstNode { } } + /// Returns an iterable containing `this` node and all [parents]. + Iterable get selfAndParents sync* { + yield this; + yield* parents; + } + /// Recursively returns all descendants of this node, e.g. its children, their /// children and so on. The tree will be pre-order traversed. Iterable get allDescendants sync* { diff --git a/sqlparser/lib/src/ast/schema/column_definition.dart b/sqlparser/lib/src/ast/schema/column_definition.dart index d875e5fb..61bd3d67 100644 --- a/sqlparser/lib/src/ast/schema/column_definition.dart +++ b/sqlparser/lib/src/ast/schema/column_definition.dart @@ -21,6 +21,15 @@ class ColumnDefinition extends AstNode { bool contentEquals(ColumnDefinition other) { return other.columnName == columnName && other.typeName == typeName; } + + /// Finds a constraint of type [T], or null, if none is set. + T findConstraint() { + final typedConstraints = constraints.whereType().iterator; + if (typedConstraints.moveNext()) { + return typedConstraints.current; + } + return null; + } } /// https://www.sqlite.org/syntax/column-constraint.html @@ -77,6 +86,9 @@ enum ConflictClause { rollback, abort, fail, ignore, replace } class NotNull extends ColumnConstraint { final ConflictClause onConflict; + Token not; + Token $null; + NotNull(String name, {this.onConflict}) : super(name); @override diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index 1d257a7e..139f7f4f 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:sqlparser/src/analysis/analysis.dart'; import 'package:sqlparser/src/ast/ast.dart'; import 'package:sqlparser/src/engine/autocomplete/engine.dart'; @@ -87,7 +89,6 @@ class SqlEngine { /// [registerTable] before calling this method. AnalysisContext analyzeParsed(ParseResult result) { final node = result.rootNode; - const SetParentVisitor().startAtRoot(node); final context = AnalysisContext(node, result.sql); final scope = _constructRootScope(); @@ -132,5 +133,39 @@ class ParseResult { final AutoCompleteEngine autoCompleteEngine; ParseResult._(this.rootNode, this.tokens, this.errors, this.sql, - this.autoCompleteEngine); + this.autoCompleteEngine) { + const SetParentVisitor().startAtRoot(rootNode); + } + + /// Attempts to find the most relevant (bottom-most in the AST) nodes that + /// intersects with the source range from [offset] to [offset] + [length]. + List findNodesAtPosition(int offset, {int length = 0}) { + if (tokens.isEmpty || rootNode == null) return const []; + + final candidates = {}; + final unchecked = Queue(); + unchecked.add(rootNode); + + while (unchecked.isNotEmpty) { + final node = unchecked.removeFirst(); + + final span = node.span; + final start = span.start.offset; + final end = span.end.offset; + + final hasIntersection = !(end < offset || start > offset + length); + if (hasIntersection) { + // this node matches. As we want to find the bottom-most node in the AST + // that matches, this means that the parent is no longer a candidate. + candidates.add(node); + candidates.remove(node.parent); + + // assume that the span of a node is a superset of the span of any + // child, so each child could potentially be interesting. + unchecked.addAll(node.childNodes); + } + } + + return candidates.toList(); + } } diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart index e0fe8cbc..a0aa9363 100644 --- a/sqlparser/lib/src/reader/parser/schema.dart +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -129,10 +129,14 @@ mixin SchemaParser on ParserBase { ..setSpan(first, _previous); } if (_matchOne(TokenType.not)) { - _consume(TokenType.$null, 'Expected NULL to complete NOT NULL'); + final notToken = _previous; + final nullToken = + _consume(TokenType.$null, 'Expected NULL to complete NOT NULL'); return NotNull(resolvedName, onConflict: _conflictClauseOrNull()) - ..setSpan(first, _previous); + ..setSpan(first, _previous) + ..not = notToken + ..$null = nullToken; } if (_matchOne(TokenType.unique)) { return UniqueColumn(resolvedName, _conflictClauseOrNull()) diff --git a/sqlparser/test/engine/find_node_by_position_test.dart b/sqlparser/test/engine/find_node_by_position_test.dart new file mode 100644 index 00000000..31d3d8bb --- /dev/null +++ b/sqlparser/test/engine/find_node_by_position_test.dart @@ -0,0 +1,19 @@ +import 'package:sqlparser/sqlparser.dart'; +import 'package:test/test.dart'; + +void main() { + test('finds the most relevant node', () { + final engine = SqlEngine(); + final result = engine.parse('SELECT * FROM tbl;'); + // | this is offset 8 + // | this is offset 17 + + final mostRelevantAtStar = result.findNodesAtPosition(8); + expect(mostRelevantAtStar.length, 1); + expect(mostRelevantAtStar.single, const TypeMatcher()); + + final mostRelevantAtTbl = result.findNodesAtPosition(17, length: 2); + expect(mostRelevantAtTbl.length, 1); + expect(mostRelevantAtTbl.single, const TypeMatcher()); + }); +}