Skip to content

Commit

Permalink
Re-host the dart web UI on v3 of the server protocol (#2691)
Browse files Browse the repository at this point in the history
re-host the dart web UI v3 of the server protocol
  • Loading branch information
devoncarew committed Nov 17, 2023
1 parent 57f8191 commit d8194c1
Show file tree
Hide file tree
Showing 18 changed files with 247 additions and 4,009 deletions.
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

0 comments on commit d8194c1

Please sign in to comment.