diff --git a/example/lib/ide/file_explorer/file_explorer.dart b/example/lib/ide/file_explorer/file_explorer.dart index 091b9d3..aa0fbaa 100644 --- a/example/lib/ide/file_explorer/file_explorer.dart +++ b/example/lib/ide/file_explorer/file_explorer.dart @@ -1,9 +1,16 @@ import 'dart:io'; import 'dart:math'; +import 'package:example/ide/ide.dart'; +import 'package:example/ide/infrastructure/user_settings.dart'; +import 'package:example/lsp_exploration/lsp/lsp_client.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; +import 'package:path/path.dart' as path; + +import '../../lsp_exploration/lsp/messages/rename_files_params.dart'; // TODO: // - switch from single, double tap recognizers to multi-tap to get rid of selection delay @@ -30,9 +37,11 @@ class FileExplorer extends StatefulWidget { super.key, required this.directory, required this.onFileOpenRequested, + required this.lspClient, }); final Directory directory; + final LspClient lspClient; final void Function(File) onFileOpenRequested; @@ -51,6 +60,8 @@ class _FileExplorerState extends State { late final List _tree; FileSystemEntityTreeViewNode? _selectedNode; + final FocusNode _treeRowFocusNode = FocusNode(); + @override void initState() { super.initState(); @@ -93,6 +104,36 @@ class _FileExplorerState extends State { } } + Future _renameFile(BuildContext context, String currentFilePath) async { + final String? newFileName = await showDialog( + context: context, + builder: (context) { + return const RenameFileDialog(); + }); + + if (newFileName == null) { + return; + } + + final newFilePath = path.join(path.dirname(currentFilePath), "$newFileName${path.extension(currentFilePath)}"); + + final params = RenameFilesParams( + files: [ + FileRename( + oldUri: "file://$currentFilePath", + newUri: "file://$newFilePath", + // TODO: Create a object to serialize the file path as understood by the LSP. Eg. file://$root/example/lib/ide/idea.dart + ), + ], + ); + + await widget.lspClient.willRenameFiles(params); + + // TODO: modify names, apply changes. + + await widget.lspClient.didRenameFiles(params); + } + static const _rowHeight = 28.0; @override @@ -201,17 +242,27 @@ class _FileExplorerState extends State { const SizedBox(width: 4.0), // Content Center( - child: Row( - children: [ - _getIconForNode(node), - const SizedBox(width: 8), - Text( - node.title, - style: const TextStyle( - fontSize: 14, + child: KeyboardListener( + focusNode: _treeRowFocusNode, + onKeyEvent: (value) { + if (value.logicalKey == LogicalKeyboardKey.enter) { + if (node.isFile) { + _renameFile(context, _selectedNode!.asFile.path); + } + } + }, + child: Row( + children: [ + _getIconForNode(node), + const SizedBox(width: 8), + Text( + node.title, + style: const TextStyle( + fontSize: 14, + ), ), - ), - ], + ], + ), ), ), ], @@ -249,13 +300,13 @@ class _FileExplorerState extends State { // Open document. setState(() { _selectedNode = node; + _treeRowFocusNode.requestFocus(); }); - if(node.isFile){ + if (node.isFile) { // Open document. widget.onFileOpenRequested(node.asFile); } - }, ), DoubleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( diff --git a/example/lib/ide/ide.dart b/example/lib/ide/ide.dart index bb08bf6..41cfe32 100644 --- a/example/lib/ide/ide.dart +++ b/example/lib/ide/ide.dart @@ -15,6 +15,8 @@ import 'package:example/lsp_exploration/lsp/messages/rename_files_params.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; + class IDE extends StatefulWidget { const IDE({ super.key, @@ -32,6 +34,8 @@ class _IDEState extends State { bool _showLeftPane = true; bool _isAnalyzing = false; + String? currentFile; + @override void initState() { super.initState(); @@ -110,6 +114,11 @@ class _IDEState extends State { workspace: widget.workspace, showLeftPane: _showLeftPane, showRightPane: true, + onFileOpen: (value) { + setState(() { + currentFile = value; + }); + }, ), ), const RightBar(), @@ -121,6 +130,7 @@ class _IDEState extends State { onLspRestartPressed: _restartLspClient, onLspStopPressed: _stopLspClient, isAnalyzing: _isAnalyzing, + currentFilePath: currentFile, ), ], ), @@ -235,28 +245,14 @@ class BottomBar extends StatelessWidget { required this.onLspRestartPressed, required this.onLspStopPressed, required this.isAnalyzing, + this.currentFilePath, }); final LspClient lspClient; final VoidCallback onLspRestartPressed; final VoidCallback onLspStopPressed; final bool isAnalyzing; - - Future _renameFile() async { - print("Rename file"); - final root = UserSettings().contentDirectory; - print('root: $root'); - final params = RenameFilesParams( - files: [ - FileRename( - oldUri: "file://$root/example/lib/ide/ide.dart", - newUri: "file://$root/example/lib/ide/idea.dart", - ), - ], - ); - final data = await lspClient.willRenameFiles(params); - print("Rename file data: $data"); - } + final String? currentFilePath; @override Widget build(BuildContext context) { @@ -314,13 +310,10 @@ class BottomBar extends StatelessWidget { Text("LSP: $_lspClientStatus"), const SizedBox(width: 8), // This is here to experiment with the LspClient - GestureDetector( - onTap: _renameFile, - child: const Icon( - Icons.science_outlined, - size: 14, - color: Colors.lightBlueAccent, - ), + const Icon( + Icons.science_outlined, + size: 14, + color: Colors.lightBlueAccent, ), const SizedBox(width: 10), GestureDetector( @@ -373,18 +366,65 @@ class BottomBar extends StatelessWidget { } } +class RenameFileDialog extends StatefulWidget { + const RenameFileDialog({ + super.key, + }); + + @override + State createState() => _RenameFileDialogState(); +} + +class _RenameFileDialogState extends State { + final TextEditingController textEditingController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: const Text("Rename file"), + children: [ + TextField( + controller: textEditingController, + decoration: const InputDecoration( + labelText: "New file name", + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(textEditingController.text); + }, + child: const Text("Rename"), + ), + ], + ), + ], + ); + } +} + class ContentArea extends StatefulWidget { const ContentArea({ super.key, required this.workspace, required this.showLeftPane, required this.showRightPane, + required this.onFileOpen, }); final Workspace workspace; final bool showLeftPane; final bool showRightPane; + final Function(String documentUrl) onFileOpen; @override State createState() => _ContentAreaState(); @@ -425,8 +465,9 @@ class _ContentAreaState extends State { ), ), child: FileExplorer( + lspClient: widget.workspace.lspClient, directory: widget.workspace.directory, - onFileOpenRequested: (file) { + onFileOpenRequested: (file) async { _fileContent.value = file.readAsStringSync(); _currentFile.value = file; @@ -437,7 +478,7 @@ class _ContentAreaState extends State { languageId = "yaml"; } - widget.workspace.lspClient.didOpenTextDocument( + await widget.workspace.lspClient.didOpenTextDocument( DidOpenTextDocumentParams( textDocument: TextDocumentItem( uri: "file://${file.absolute.path}", @@ -447,6 +488,8 @@ class _ContentAreaState extends State { ), ), ); + + widget.onFileOpen(_currentFile.value!.path); }, ), ), diff --git a/example/lib/lsp_exploration/lsp/lsp_client.dart b/example/lib/lsp_exploration/lsp/lsp_client.dart index c599b61..2722d1c 100644 --- a/example/lib/lsp_exploration/lsp/lsp_client.dart +++ b/example/lib/lsp_exploration/lsp/lsp_client.dart @@ -242,6 +242,15 @@ class LspClient with ChangeNotifier { return data.value.map((e) => LspCodeAction.fromJson(e)).toList(); } + + Future didRenameFiles(RenameFilesParams params) async { + final requestData = params.toJson(); + + await _lspClientCommunication!.sendNotification( + 'workspace/didRenameFiles', + requestData, + ); + } } class LspJsonRpcClient { diff --git a/example/lib/lsp_exploration/lsp/messages/initialize.dart b/example/lib/lsp_exploration/lsp/messages/initialize.dart index 7541893..e57a2ef 100644 --- a/example/lib/lsp_exploration/lsp/messages/initialize.dart +++ b/example/lib/lsp_exploration/lsp/messages/initialize.dart @@ -133,8 +133,10 @@ class LspClientCapabilities { 'documentChanges': true, 'resourceOperations': ['create', 'rename', 'delete'] }, + 'fileOperations': { 'willRename': true, + 'didRename': true, } }, }; diff --git a/pubspec.yaml b/pubspec.yaml index 48a0f60..a4625c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: analyzer: ^6.4.1 flutter: sdk: flutter + path: ^1.9.0 dev_dependencies: flutter_test: