12 changed files with 673 additions and 295 deletions
@ -0,0 +1,163 @@ |
|||||||
|
import 'dart:convert'; |
||||||
|
import 'dart:io'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/services.dart'; |
||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'text_editor_controller.dart'; |
||||||
|
|
||||||
|
class TextEditorActions { |
||||||
|
static Future<void> openFile(BuildContext context, TextEditorController controller) async { |
||||||
|
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); |
||||||
|
|
||||||
|
if (result != null && result.files.single.path != null) { |
||||||
|
await _loadFile(context, controller, result.files.single.path!); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static Future<void> copyToClipboard(BuildContext context, String content) async { |
||||||
|
await Clipboard.setData(ClipboardData(text: content)); |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static Future<void> saveFile(BuildContext context, String content) async { |
||||||
|
try { |
||||||
|
String? outputPath = await FilePicker.platform.saveFile( |
||||||
|
dialogTitle: '保存文件', |
||||||
|
fileName: 'untitled.txt', |
||||||
|
allowedExtensions: ['txt'], |
||||||
|
type: FileType.any, |
||||||
|
); |
||||||
|
|
||||||
|
if (outputPath == null) return; |
||||||
|
|
||||||
|
final file = File(outputPath); |
||||||
|
|
||||||
|
if (await file.exists()) { |
||||||
|
final shouldOverwrite = await showDialog<bool>( |
||||||
|
context: context, |
||||||
|
builder: |
||||||
|
(context) => AlertDialog( |
||||||
|
title: const Text('文件已存在'), |
||||||
|
content: const Text('要覆盖现有文件吗?'), |
||||||
|
actions: [ |
||||||
|
TextButton( |
||||||
|
onPressed: () => Navigator.pop(context, false), |
||||||
|
child: const Text('取消'), |
||||||
|
), |
||||||
|
TextButton( |
||||||
|
onPressed: () => Navigator.pop(context, true), |
||||||
|
child: const Text('覆盖'), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
|
||||||
|
if (shouldOverwrite != true) return; |
||||||
|
} |
||||||
|
|
||||||
|
await file.writeAsString(content); |
||||||
|
|
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}'))); |
||||||
|
} |
||||||
|
} on FileSystemException catch (e) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}'))); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of( |
||||||
|
context, |
||||||
|
).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static Future<void> _loadFile( |
||||||
|
BuildContext context, |
||||||
|
TextEditorController controller, |
||||||
|
String filePath, |
||||||
|
) async { |
||||||
|
try { |
||||||
|
controller.setLoading(true); |
||||||
|
final file = File(filePath); |
||||||
|
final fileSize = await file.length(); |
||||||
|
|
||||||
|
if (fileSize > controller.maxFileSize) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of( |
||||||
|
context, |
||||||
|
).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!controller.isEmpty) { |
||||||
|
final confirm = await showDialog<bool>( |
||||||
|
context: context, |
||||||
|
builder: |
||||||
|
(context) => AlertDialog( |
||||||
|
title: const Text('确认提示'), |
||||||
|
content: const Text('确认是否打开新的文件?打开后将覆盖当前内容'), |
||||||
|
actions: [ |
||||||
|
TextButton( |
||||||
|
onPressed: () => Navigator.pop(context, false), |
||||||
|
child: const Text('取消'), |
||||||
|
), |
||||||
|
TextButton( |
||||||
|
onPressed: () => Navigator.pop(context, true), |
||||||
|
child: const Text('确认'), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
|
||||||
|
if (confirm != true) return; |
||||||
|
} |
||||||
|
|
||||||
|
final fileName = file.path.split('\\').last; |
||||||
|
controller.updateContent(''); |
||||||
|
controller.onContentChanged?.call('', fileName); |
||||||
|
|
||||||
|
final stream = file.openRead(); |
||||||
|
final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); |
||||||
|
|
||||||
|
await for (final line in lines) { |
||||||
|
if (!controller.textController.hasListeners) break; |
||||||
|
|
||||||
|
controller.textController.text += '$line\n'; |
||||||
|
controller.onContentChanged?.call(controller.content, fileName); |
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||||
|
controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent); |
||||||
|
}); |
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10)); |
||||||
|
} |
||||||
|
|
||||||
|
controller.onFileLoaded?.call(file.path); |
||||||
|
|
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); |
||||||
|
} |
||||||
|
} on FormatException { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
||||||
|
} |
||||||
|
} on FileSystemException catch (e) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
if (context.mounted) { |
||||||
|
ScaffoldMessenger.of( |
||||||
|
context, |
||||||
|
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); |
||||||
|
} |
||||||
|
} finally { |
||||||
|
controller.setLoading(false); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
class TextEditorController { |
||||||
|
final TextEditingController textController; |
||||||
|
final FocusNode focusNode; |
||||||
|
final ScrollController scrollController; |
||||||
|
final Function(String, String?)? onContentChanged; |
||||||
|
final Function(String)? onFileLoaded; |
||||||
|
|
||||||
|
bool _isLoading = false; |
||||||
|
static const int _maxFileSize = 1024 * 1024; // 1MB |
||||||
|
|
||||||
|
TextEditorController({String? initialContent, this.onContentChanged, this.onFileLoaded}) |
||||||
|
: textController = TextEditingController(text: initialContent ?? ''), |
||||||
|
focusNode = FocusNode(), |
||||||
|
scrollController = ScrollController(); |
||||||
|
|
||||||
|
bool get isLoading => _isLoading; |
||||||
|
bool get hasFocus => focusNode.hasFocus; |
||||||
|
bool get isEmpty => textController.text.isEmpty; |
||||||
|
String get content => textController.text; |
||||||
|
int get contentLength => textController.text.length; |
||||||
|
int get maxFileSize => _maxFileSize; |
||||||
|
|
||||||
|
void updateContent(String content) { |
||||||
|
textController.text = content; |
||||||
|
} |
||||||
|
|
||||||
|
void handleContentChanged(String text) { |
||||||
|
onContentChanged?.call(text, null); |
||||||
|
} |
||||||
|
|
||||||
|
void setLoading(bool loading) { |
||||||
|
_isLoading = loading; |
||||||
|
} |
||||||
|
|
||||||
|
void dispose() { |
||||||
|
textController.dispose(); |
||||||
|
focusNode.dispose(); |
||||||
|
scrollController.dispose(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/core/tab_manager.dart'; |
||||||
|
|
||||||
|
class ContentSearchController { |
||||||
|
final TabManager tabManager; |
||||||
|
|
||||||
|
String searchQuery = ''; |
||||||
|
String searchDirectory = ''; |
||||||
|
String fileType = '*.*'; |
||||||
|
bool caseSensitive = false; |
||||||
|
bool wholeWord = false; |
||||||
|
bool useRegex = false; |
||||||
|
SearchMode searchMode = SearchMode.locate; |
||||||
|
final List<SearchResult> results = []; |
||||||
|
|
||||||
|
ContentSearchController({required this.tabManager}); |
||||||
|
|
||||||
|
Future<void> startSearch() async { |
||||||
|
results.clear(); |
||||||
|
// 模拟搜索结果 |
||||||
|
results.addAll([ |
||||||
|
SearchResult( |
||||||
|
filePath: 'lib/main.dart', |
||||||
|
lineNumber: 42, |
||||||
|
lineContent: 'void main() => runApp(MyApp());', |
||||||
|
matches: const [MatchResult(start: 5, end: 9)], |
||||||
|
), |
||||||
|
SearchResult( |
||||||
|
filePath: 'lib/home_page.dart', |
||||||
|
lineNumber: 17, |
||||||
|
lineContent: 'class HomePage extends StatelessWidget {', |
||||||
|
matches: const [MatchResult(start: 6, end: 10)], |
||||||
|
), |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> pickDirectory() async { |
||||||
|
final dir = await FilePicker.platform.getDirectoryPath(); |
||||||
|
if (dir != null) { |
||||||
|
searchDirectory = dir; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum SearchMode { locate, count } |
||||||
|
|
||||||
|
class SearchResult { |
||||||
|
final String filePath; |
||||||
|
final int lineNumber; |
||||||
|
final String lineContent; |
||||||
|
final List<MatchResult> matches; |
||||||
|
|
||||||
|
SearchResult({ |
||||||
|
required this.filePath, |
||||||
|
required this.lineNumber, |
||||||
|
required this.lineContent, |
||||||
|
required this.matches, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
class MatchResult { |
||||||
|
final int start; |
||||||
|
final int end; |
||||||
|
|
||||||
|
const MatchResult({required this.start, required this.end}); |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/app/core/tab_manager.dart'; |
||||||
|
import 'content_search_controller.dart'; |
||||||
|
import 'directory_settings.dart'; |
||||||
|
import 'search_settings.dart'; |
||||||
|
import 'results_view.dart'; |
||||||
|
|
||||||
|
class ContentSearchView extends StatefulWidget { |
||||||
|
final String tabId; |
||||||
|
|
||||||
|
const ContentSearchView({super.key, required this.tabId}); |
||||||
|
|
||||||
|
@override |
||||||
|
State<ContentSearchView> createState() => ContentSearchViewState(); |
||||||
|
} |
||||||
|
|
||||||
|
class ContentSearchViewState extends State<ContentSearchView> { |
||||||
|
late final ContentSearchController _controller; |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
_controller = ContentSearchController( |
||||||
|
tabManager: Provider.of<TabManager>(context, listen: false), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
DirectorySettings(controller: _controller), |
||||||
|
const SizedBox(height: 16), |
||||||
|
SearchSettings(controller: _controller), |
||||||
|
const SizedBox(height: 16), |
||||||
|
Expanded(child: ResultsView(controller: _controller)), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; |
||||||
|
|
||||||
|
class DirectorySettings extends StatelessWidget { |
||||||
|
final ContentSearchController controller; |
||||||
|
|
||||||
|
const DirectorySettings({super.key, required this.controller}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Card( |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
const Icon(Icons.folder, color: Colors.blue), |
||||||
|
const SizedBox(width: 8), |
||||||
|
Expanded( |
||||||
|
child: TextField( |
||||||
|
decoration: const InputDecoration(labelText: '搜索目录', border: OutlineInputBorder()), |
||||||
|
onChanged: (value) => controller.searchDirectory = value, |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 8), |
||||||
|
SizedBox( |
||||||
|
width: 100, |
||||||
|
child: TextField( |
||||||
|
decoration: const InputDecoration( |
||||||
|
labelText: '文件类型', |
||||||
|
border: OutlineInputBorder(), |
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 12), |
||||||
|
), |
||||||
|
controller: TextEditingController(text: controller.fileType), |
||||||
|
onChanged: (value) => controller.fileType = value, |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 8), |
||||||
|
IconButton(icon: const Icon(Icons.folder_open), onPressed: controller.pickDirectory), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; |
||||||
|
|
||||||
|
class ResultsView extends StatelessWidget { |
||||||
|
final ContentSearchController controller; |
||||||
|
|
||||||
|
const ResultsView({super.key, required this.controller}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Card( |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Text( |
||||||
|
controller.searchMode == SearchMode.locate ? '定位结果' : '计数结果', |
||||||
|
style: Theme.of(context).textTheme.titleMedium, |
||||||
|
), |
||||||
|
), |
||||||
|
Expanded( |
||||||
|
child: |
||||||
|
controller.searchMode == SearchMode.locate |
||||||
|
? _buildLocateResults() |
||||||
|
: _buildCountResults(), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildLocateResults() { |
||||||
|
return ListView.builder( |
||||||
|
itemCount: controller.results.length, |
||||||
|
itemBuilder: (ctx, index) { |
||||||
|
final result = controller.results[index]; |
||||||
|
return ExpansionTile( |
||||||
|
title: Text('${result.filePath}:${result.lineNumber}'), |
||||||
|
children: [ |
||||||
|
Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Text.rich(_buildHighlightedText(result.lineContent, result.matches)), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildCountResults() { |
||||||
|
final counts = <String, int>{}; |
||||||
|
for (var r in controller.results) { |
||||||
|
counts[r.filePath] = (counts[r.filePath] ?? 0) + r.matches.length; |
||||||
|
} |
||||||
|
|
||||||
|
return ListView.builder( |
||||||
|
itemCount: counts.length, |
||||||
|
itemBuilder: (ctx, index) { |
||||||
|
final entry = counts.entries.elementAt(index); |
||||||
|
return ListTile( |
||||||
|
leading: const Icon(Icons.insert_drive_file), |
||||||
|
title: Text(entry.key), |
||||||
|
trailing: Chip(label: Text('${entry.value}处')), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
TextSpan _buildHighlightedText(String text, List<MatchResult> matches) { |
||||||
|
final spans = <TextSpan>[]; |
||||||
|
int lastEnd = 0; |
||||||
|
|
||||||
|
for (final match in matches) { |
||||||
|
if (match.start > lastEnd) { |
||||||
|
spans.add(TextSpan(text: text.substring(lastEnd, match.start))); |
||||||
|
} |
||||||
|
spans.add( |
||||||
|
TextSpan( |
||||||
|
text: text.substring(match.start, match.end), |
||||||
|
style: const TextStyle(backgroundColor: Colors.yellow, fontWeight: FontWeight.bold), |
||||||
|
), |
||||||
|
); |
||||||
|
lastEnd = match.end; |
||||||
|
} |
||||||
|
if (lastEnd < text.length) { |
||||||
|
spans.add(TextSpan(text: text.substring(lastEnd))); |
||||||
|
} |
||||||
|
|
||||||
|
return TextSpan(children: spans); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/components/text_editor.dart'; |
||||||
|
import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; |
||||||
|
|
||||||
|
class SearchSettings extends StatelessWidget { |
||||||
|
final ContentSearchController controller; |
||||||
|
final GlobalKey<TextEditorState> _searchEditorKey = GlobalKey(); |
||||||
|
|
||||||
|
SearchSettings({super.key, required this.controller}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Card( |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Row( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
// 搜索内容框 |
||||||
|
SizedBox( |
||||||
|
width: MediaQuery.of(context).size.width * 0.5, // 占据一半宽度 |
||||||
|
height: 300, |
||||||
|
child: TextEditor( |
||||||
|
key: _searchEditorKey, |
||||||
|
tabId: 'search_content', |
||||||
|
title: '搜索内容', |
||||||
|
onContentChanged: (content, _) => controller.searchQuery = content, |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 8), |
||||||
|
// 设置按钮区域 |
||||||
|
Expanded( |
||||||
|
child: Container( |
||||||
|
decoration: BoxDecoration( |
||||||
|
border: Border.all(color: Colors.grey), // 设置边框颜色 |
||||||
|
borderRadius: BorderRadius.circular(4), // 设置边框圆角 |
||||||
|
), |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
// 搜索方式 |
||||||
|
Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
const Text('搜索方式:', style: TextStyle(fontSize: 12)), |
||||||
|
Row( |
||||||
|
children: [ |
||||||
|
Radio<SearchMode>( |
||||||
|
value: SearchMode.locate, |
||||||
|
groupValue: controller.searchMode, |
||||||
|
onChanged: (value) => controller.searchMode = value!, |
||||||
|
), |
||||||
|
const Text('定位', style: TextStyle(fontSize: 12)), |
||||||
|
], |
||||||
|
), |
||||||
|
Row( |
||||||
|
children: [ |
||||||
|
Radio<SearchMode>( |
||||||
|
value: SearchMode.count, |
||||||
|
groupValue: controller.searchMode, |
||||||
|
onChanged: (value) => controller.searchMode = value!, |
||||||
|
), |
||||||
|
const Text('计数', style: TextStyle(fontSize: 12)), |
||||||
|
], |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
const SizedBox(height: 8), |
||||||
|
// 匹配规则 |
||||||
|
Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
const Text('匹配规则:', style: TextStyle(fontSize: 12)), |
||||||
|
CheckboxListTile( |
||||||
|
contentPadding: EdgeInsets.zero, |
||||||
|
controlAffinity: ListTileControlAffinity.leading, |
||||||
|
title: const Text('大小写敏感', style: TextStyle(fontSize: 12)), |
||||||
|
value: controller.caseSensitive, |
||||||
|
onChanged: (value) => controller.caseSensitive = value!, |
||||||
|
), |
||||||
|
CheckboxListTile( |
||||||
|
contentPadding: EdgeInsets.zero, |
||||||
|
controlAffinity: ListTileControlAffinity.leading, |
||||||
|
title: const Text('全字匹配', style: TextStyle(fontSize: 12)), |
||||||
|
value: controller.wholeWord, |
||||||
|
onChanged: (value) => controller.wholeWord = value!, |
||||||
|
), |
||||||
|
CheckboxListTile( |
||||||
|
contentPadding: EdgeInsets.zero, |
||||||
|
controlAffinity: ListTileControlAffinity.leading, |
||||||
|
title: const Text('正则匹配', style: TextStyle(fontSize: 12)), |
||||||
|
value: controller.useRegex, |
||||||
|
onChanged: (value) => controller.useRegex = value!, |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
const SizedBox(height: 8), |
||||||
|
// 开始搜索按钮 |
||||||
|
Align( |
||||||
|
alignment: Alignment.centerLeft, |
||||||
|
child: ElevatedButton.icon( |
||||||
|
icon: const Icon(Icons.search, size: 20), |
||||||
|
label: const Text('开始搜索'), |
||||||
|
onPressed: controller.startSearch, |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue