Add autocomplete suggestions for HTTP headers in request editor

This commit is contained in:
wanghongenpin
2026-02-27 01:00:03 +08:00
parent 905d8932bd
commit 6bf5063bed
4 changed files with 412 additions and 59 deletions

View File

@@ -208,7 +208,7 @@ class RequestEditorState extends State<RequestEditor> {
]))
]),
message: response,
readOnly: widget.source == RequestEditorSource.breakpointRequest))
readOnly: widget.source != RequestEditorSource.breakpointResponse))
],
);
}),
@@ -377,7 +377,7 @@ class _HttpState extends State<_HttpWidget> {
}
}
change(HttpMessage? message) {
void change(HttpMessage? message) {
this.message = message;
body?.text = message?.bodyAsString ?? '';
headerKey.currentState?.refreshParam(message?.headers.getHeaders());
@@ -416,7 +416,8 @@ class _HttpState extends State<_HttpWidget> {
KeyValWidget(
key: headerKey,
params: message?.headers.getHeaders() ?? initHeader,
readOnly: widget.readOnly),
readOnly: widget.readOnly,
suggestions: HttpHeaders.commonHeaderKeys),
_body()
],
)),
@@ -521,6 +522,8 @@ class KeyVal {
bool enabled = true;
TextEditingController key;
TextEditingController value;
FocusNode? keyFocusNode;
FocusNode? valueFocusNode;
KeyVal(this.key, this.value);
}
@@ -530,8 +533,9 @@ class KeyValWidget extends StatefulWidget {
final Map<String, List<String>>? params;
final bool readOnly; //只读
final UrlQueryNotifier? paramNotifier;
final List<String>? suggestions;
const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier});
const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier, this.suggestions});
@override
State<StatefulWidget> createState() => KeyValState();
@@ -570,7 +574,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
}
//监听url发生变化 更改表单
onChange(String value) {
void onChange(String value) {
var query = value.split("&");
int index = 0;
while (index < query.length) {
@@ -591,7 +595,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
setState(() {});
}
notifierChange() {
void notifierChange() {
if (widget.paramNotifier == null) return;
String query = _params
.where((e) => e.enabled && e.key.text.isNotEmpty)
@@ -600,7 +604,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
widget.paramNotifier?.onParamChange(query);
}
clear() {
void clear() {
for (var element in _params) {
element.key.dispose();
element.value.dispose();
@@ -609,7 +613,7 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
}
//刷新param
refreshParam(Map<String, List<String>>? headers) {
void refreshParam(Map<String, List<String>>? headers) {
clear();
setState(() {
headers?.forEach((name, values) {
@@ -692,13 +696,103 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
return list;
}
Widget _cell(TextEditingController val, {bool isKey = false}) {
Widget _cell(KeyVal keyVal,
{bool isKey = false,
FocusNode? focusNode,
List<String>? suggestions,
Map<String, List<String>>? valueSuggestions}) {
TextEditingController textController = isKey ? keyVal.key : keyVal.value;
if (!widget.readOnly && (suggestions != null || valueSuggestions != null)) {
return Container(
padding: const EdgeInsets.only(right: 5),
child: RawAutocomplete<String>(
textEditingController: textController,
focusNode: focusNode,
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
var currentSuggestions = suggestions;
if (!isKey && valueSuggestions?.containsKey(keyVal.key.text) == true) {
currentSuggestions = valueSuggestions![keyVal.key.text];
}
if (currentSuggestions == null) {
return const Iterable<String>.empty();
}
return currentSuggestions.where((String option) {
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
});
},
onSelected: (String selection) {
textController.text = selection;
notifierChange();
},
fieldViewBuilder: (BuildContext context, TextEditingController textEditingController,
FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) {
return TextFormField(
controller: textEditingController,
focusNode: fieldFocusNode,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
onChanged: (val) {
if (isKey) setState(() {});
notifierChange();
},
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
minLines: 1,
maxLines: 3,
decoration: InputDecoration(
isDense: true,
hintStyle: const TextStyle(color: Colors.grey),
contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),
border: InputBorder.none,
hintText: isKey ? "Key" : "Value"));
},
optionsViewBuilder:
(BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final String option = options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Container(
padding: const EdgeInsets.all(10.0),
child: _buildHighlightText(option, textController.text),
),
);
},
),
),
),
);
},
));
}
return Container(
padding: const EdgeInsets.only(right: 5),
child: TextFormField(
readOnly: widget.readOnly,
style: TextStyle(fontSize: 13, fontWeight: isKey ? FontWeight.w500 : null),
controller: val,
controller: textController,
onChanged: (val) => notifierChange(),
minLines: 1,
maxLines: 3,
@@ -715,6 +809,16 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
}
Widget _row(KeyVal keyVal, Widget? op) {
if (widget.suggestions != null) {
keyVal.keyFocusNode ??= FocusNode();
}
Map<String, List<String>>? valueSuggestions;
if (widget.suggestions != null) {
keyVal.valueFocusNode ??= FocusNode();
valueSuggestions = HttpHeaders.commonHeaderValues;
}
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
if (op != null)
Checkbox(
@@ -726,11 +830,31 @@ class KeyValState extends State<KeyValWidget> with AutomaticKeepAliveClientMixin
notifierChange();
}),
Container(width: 5),
Expanded(flex: 4, child: _cell(keyVal.key, isKey: true)),
Expanded(
flex: 4, child: _cell(keyVal, isKey: true, suggestions: widget.suggestions, focusNode: keyVal.keyFocusNode)),
const Text(":", style: TextStyle(color: Colors.deepOrangeAccent)),
const SizedBox(width: 8),
Expanded(flex: 6, child: _cell(keyVal.value)),
Expanded(flex: 6, child: _cell(keyVal, focusNode: keyVal.valueFocusNode, valueSuggestions: valueSuggestions)),
op ?? const SizedBox()
]);
}
Widget _buildHighlightText(String text, String query) {
if (query.isEmpty) {
return Text(text);
}
int index = text.toLowerCase().indexOf(query.toLowerCase());
if (index < 0) {
return Text(text);
}
return Text.rich(TextSpan(children: [
TextSpan(text: text.substring(0, index)),
TextSpan(
text: text.substring(index, index + query.length),
style: TextStyle(color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold)),
TextSpan(text: text.substring(index + query.length))
]));
}
}