Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-host the dart web UI on v3 of the server protocol #2691

Merged
merged 12 commits into from
Nov 17, 2023
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pkgs/dart_pad/lib/src/protos/** linguist-generated=true
pkgs/dart_services/lib/src/shared/** linguist-generated=true
pkgs/sketch_pad/lib/samples.g.dart linguist-generated=true
pkgs/sketch_pad/web/codemirror/** linguist-generated=true
286 changes: 117 additions & 169 deletions pkgs/dart_pad/lib/completion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,237 +2,187 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert' show jsonDecode;
import 'package:async/async.dart';
import 'package:dartpad_shared/services.dart' as ds;

import 'editing/editor.dart';
import 'services/dartservices.dart' as ds;

// TODO: For CodeMirror, we get a request each time the user hits a key when the
// completion popup is open. We need to cache the results when appropriate.

class DartCompleter extends CodeCompleter {
final ds.DartservicesApi servicesApi;
final ds.ServicesClient servicesApi;
final Document document;

CancelableCompleter<CompletionResult>? _lastCompleter;

DartCompleter(this.servicesApi, this.document);

@override
Future<CompletionResult> complete(
Editor editor, {
bool onlyShowFixes = false,
}) {
// Cancel any open completion request.
_lastCompleter?.operation.cancel();

}) async {
final offset = editor.document.indexFromPos(editor.document.cursor);

final request = ds.SourceRequest()
..source = editor.document.value
..offset = offset;

final completer = CancelableCompleter<CompletionResult>();
_lastCompleter = completer;
final request = ds.SourceRequest(
source: editor.document.value,
offset: offset,
);

if (onlyShowFixes) {
final response = await servicesApi.fixes(request);
final completions = <Completion>[];
final fixesFuture =
servicesApi.fixes(request).then((ds.FixesResponse response) {
for (final problemFix in response.fixes) {
for (final fix in problemFix.fixes) {
final fixes = fix.edits.map((edit) {
return SourceEdit(edit.length, edit.offset, edit.replacement);
}).toList();

completions.add(Completion(
'',
displayString: fix.message,
type: 'type-quick_fix',
quickFixes: fixes,
));
}

for (final sourceChange in response.fixes) {
final fixes = sourceChange.edits.map((edit) {
return SourceEdit(edit.length, edit.offset, edit.replacement);
}).toList();

completions.add(Completion(
'',
displayString: sourceChange.message,
type: 'type-quick_fix',
quickFixes: fixes,
));
}

for (final assist in response.assists) {
final sourceEdits = assist.edits
.map((edit) =>
SourceEdit(edit.length, edit.offset, edit.replacement))
.toList();

int? absoluteCursorPosition;

// TODO(redbrogdon): Find a way to properly use these linked edit
// groups via selections and multiple cursors.
if (assist.linkedEditGroups.isNotEmpty) {
absoluteCursorPosition = assist.linkedEditGroups.first.offsets.first;
}

// If a specific offset is provided, prefer it to the one calculated
// from the linked edit groups.
if (assist.selectionOffset != null) {
absoluteCursorPosition = assist.selectionOffset;
}

final completion = Completion(
'',
displayString: assist.message,
type: 'type-quick_fix',
quickFixes: sourceEdits,
absoluteCursorPosition: absoluteCursorPosition,
);

completions.add(completion);
}

return CompletionResult(
completions,
replaceOffset: offset,
replaceLength: 0,
);
} else {
final response = await servicesApi.complete(request);
final replaceOffset = response.replacementOffset;
final replaceLength = response.replacementLength;

final responses = response.suggestions.map((suggestion) {
return AnalysisCompletion(replaceOffset, replaceLength, suggestion);
});
final assistsFuture =
servicesApi.assists(request).then((ds.AssistsResponse response) {
for (final assist in response.assists) {
final sourceEdits = assist.edits
.map((edit) =>
SourceEdit(edit.length, edit.offset, edit.replacement))
.toList();

int? absoluteCursorPosition;

// TODO(redbrogdon): Find a way to properly use these linked edit
// groups via selections and multiple cursors.
if (assist.linkedEditGroups.isNotEmpty) {
absoluteCursorPosition =
assist.linkedEditGroups.first.positions.first;
}

// If a specific offset is provided, prefer it to the one calculated
// from the linked edit groups.
if (assist.hasSelectionOffset()) {
absoluteCursorPosition = assist.selectionOffset;
}
final completions = responses.map((completion) {
var displayString = completion.isMethod
? '${completion.text}${completion.parameters}'
: completion.text;
if (completion.isMethod && completion.returnType != null) {
displayString += ' → ${completion.returnType}';
}

final completion = Completion(
'',
displayString: assist.message,
type: 'type-quick_fix',
quickFixes: sourceEdits,
absoluteCursorPosition: absoluteCursorPosition,
);
var text = completion.text;

completions.add(completion);
if (completion.isMethod) {
text += '()';
}
});

Future.wait([fixesFuture, assistsFuture]).then((_) {
completer.complete(CompletionResult(completions,
replaceOffset: offset, replaceLength: 0));
});
} else {
servicesApi.complete(request).then<void>((ds.CompleteResponse response) {
if (completer.isCanceled) return;

final replaceOffset = response.replacementOffset;
final replaceLength = response.replacementLength;

final responses = response.completions.map((completion) {
return AnalysisCompletion(replaceOffset, replaceLength, completion);
});

final completions = responses.map((completion) {
// TODO: Move to using a LabelProvider; decouple the data and rendering.
var displayString = completion.isMethod
? '${completion.text}${completion.parameters}'
: completion.text;
if (completion.isMethod && completion.returnType != null) {
displayString += ' → ${completion.returnType}';
}
if (completion.isConstructor) {
displayString += '()';
}

var text = completion.text;
final deprecatedClass = completion.isDeprecated ? ' deprecated' : '';

if (completion.isMethod) {
text += '()';
}
int? cursorPos;

if (completion.isConstructor) {
displayString += '()';
}
if (completion.isMethod && completion.parameterCount! > 0) {
cursorPos = text.indexOf('(') + 1;
}

final deprecatedClass = completion.isDeprecated ? ' deprecated' : '';

if (completion.type == null) {
return Completion(
text,
displayString: displayString,
type: deprecatedClass,
);
} else {
int? cursorPos;

if (completion.isMethod && completion.parameterCount! > 0) {
cursorPos = text.indexOf('(') + 1;
}

if (completion.selectionOffset != 0) {
cursorPos = completion.selectionOffset;
}

return Completion(
text,
displayString: displayString,
type: 'type-${completion.type!.toLowerCase()}$deprecatedClass',
cursorOffset: cursorPos,
);
}
}).toList();
if (completion.selectionOffset != 0) {
cursorPos = completion.selectionOffset;
}

// Removes duplicates when a completion is both a getter and a setter.
for (final completion in completions) {
for (final other in completions) {
if (completion.isSetterAndMatchesGetter(other)) {
completions.removeWhere((c) => completion == c);
other.type = 'type-getter_and_setter';
}
return Completion(
text,
displayString: displayString,
type: 'type-${completion.type.toLowerCase()}$deprecatedClass',
cursorOffset: cursorPos,
);
}).toList();

// Removes duplicates when a completion is both a getter and a setter.
for (final completion in completions) {
for (final other in completions) {
if (completion.isSetterAndMatchesGetter(other)) {
completions.removeWhere((c) => completion == c);
other.type = 'type-getter_and_setter';
}
}
}

completer.complete(CompletionResult(
completions,
replaceOffset: replaceOffset,
replaceLength: replaceLength,
));
}).catchError(completer.completeError);
return CompletionResult(
completions,
replaceOffset: replaceOffset,
replaceLength: replaceLength,
);
}

return completer.operation.value;
}
}

class AnalysisCompletion implements Comparable<AnalysisCompletion> {
final int offset;
final int length;
final ds.CompletionSuggestion suggestion;

final Map<String, dynamic> _map;

AnalysisCompletion(this.offset, this.length, ds.Completion completion)
: _map = Map<String, dynamic>.from(completion.completion) {
// TODO: We need to pass this completion info better.
_convert('element');
_convert('parameterNames');
_convert('parameterTypes');

if (_map.containsKey('element')) {
_element.remove('location');
}
}

Map<String, dynamic> get _element => _map['element'] as Map<String, dynamic>;

// Convert maps and lists that have been passed as json.
void _convert(String key) {
if (_map[key] is String) {
_map[key] = jsonDecode(_map[key] as String);
}
}
AnalysisCompletion(this.offset, this.length, this.suggestion);

// KEYWORD, INVOCATION, ...
String? get kind => _map['kind'] as String?;
String get kind => suggestion.kind;

bool get isMethod =>
_element['kind'] == 'FUNCTION' || _element['kind'] == 'METHOD';
suggestion.elementKind == 'FUNCTION' ||
suggestion.elementKind == 'METHOD';

bool get isConstructor => type == 'CONSTRUCTOR';

String? get parameters => isMethod ? _element['parameters'] as String? : null;
String? get parameters => isMethod ? suggestion.elementParameters : null;

int? get parameterCount =>
// ignore: avoid_dynamic_calls
isMethod ? _map['parameterNames'].length as int? : null;
isMethod ? suggestion.parameterNames?.length : null;

String get text {
final str = _map['completion'] as String;
final str = suggestion.completion;
if (str.startsWith('(') && str.endsWith(')')) {
return str.substring(1, str.length - 1);
} else {
return str;
}
}

String? get returnType => _map['returnType'] as String?;
String? get returnType => suggestion.returnType;

bool get isDeprecated => _map['isDeprecated'] == 'true';
bool get isDeprecated => suggestion.deprecated;

int get selectionOffset => _int(_map['selectionOffset'] as String?);
int get selectionOffset => suggestion.selectionOffset;

// FUNCTION, GETTER, CLASS, ...
String? get type =>
_map.containsKey('element') ? _element['kind'] as String? : kind;
String get type => suggestion.elementKind ?? kind;

@override
int compareTo(AnalysisCompletion other) {
Expand All @@ -241,6 +191,4 @@ class AnalysisCompletion implements Comparable<AnalysisCompletion> {

@override
String toString() => text;

int _int(String? val) => val == null ? 0 : int.parse(val);
}
2 changes: 1 addition & 1 deletion pkgs/dart_pad/lib/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'services/dartservices.dart';
import 'package:dartpad_shared/services.dart';

abstract class ContextBase {
bool get isFocused;
Expand Down
Loading
Loading