I think I probably ought to use Dart “parts” in the ICE Code Editor. Parts are used to organize a library like ICE. When I first started writing the Dart version of the editor, I mostly followed along with the old JavaScript version. JavaScript, of course, has no formal mechanism for code organization so code written in it is hardly a worthwhile guide. Now that I have two classes in ICE and am ready to add another, it seems time to give this some consideration.
Without parts, developers would have to import classes individually:
import 'package:ice_code_editor/editor.dart'; import 'package:ice_code_editor/embedded.dart'; import 'package:ice_code_editor/full.dart'; import 'package:ice_code_editor/store.dart'; // ... new Editor(); new Embedded(); new FullScreen(); new Store();If the individual classes were all in a single file, then only one import would be necessary:
import 'package:ice_code_editor/ice.dart'; // ... new Editor(); new Embedded(); new FullScreen(); new Store();Now, I do not think that most use-cases of ICE warrant using more than a single class at a time. Still, reducing the
import
to a single line would make documentation easier and would make it easier for developers to remember the package path. Of course, it would be horrendous from a code maintenance standpoint to put everything in a single file—no one in their right mind thinks a thousand line long file of code is anything even approaching maintainable. This is the very point of Dart parts. They let me write the different classes in their own files, but mark them as being a part of a single library file.
In this case, I want to make the
editor.dart
and stort.dart
file part of the main ice.dart
file that will be imported into other code. So, in ice.dart
, I indicate that these two files are parts:library ice; part 'editor.dart'; part 'store.dart';The
editor.dart
file needs to change to support this. It is no longer a standalone library. It is now part of the ice
library. So I need to remove the library
declaration at the top of editor.dart
:library ice_editor; import 'dart:html'; import 'dart:async'; import 'package:js/js.dart' as js; import 'package:js/js_wrapping.dart' as jsw; class Editor { // code here... }And mark it as a “part of” ice:
part of ice; import 'dart:html'; import 'dart:async'; import 'package:js/js.dart' as js; import 'package:js/js_wrapping.dart' as jsw; class Editor { // code here... }That's not the end of it. Parts cannot have
import
statements, so I remove them from editor.dart
:part of ice; class Editor { // code here... }After making similar changes to
store.dart
, I move all of import
statements into ice.dart
:library ice; import 'dart:async'; import 'dart:collection'; import 'dart:crypto'; import 'dart:html'; import 'dart:json' as JSON; import 'package:js/js.dart' as js; import 'package:js/js_wrapping.dart' as jsw; part 'editor.dart'; part 'store.dart';Lastly, I update my tests to import the new
ice.dart
:import 'package:unittest/unittest.dart'; import 'dart:html'; import 'package:ice_code_editor/ice.dart'; // ...With that, I have my code nicely organized and all my tests passing. Unfortunately, this makes
dart_analyzer
profoundly unhappy. Since I use Emacs for all of my editing, I rely on the dart_analyzer
to ensure that I have not made any silly type errors. For instance, if I called a non-existent method on a List:part of ice; class Store implements HashMap<String, HashMap> { // ... // There is no "iEmpty" method on the projects List: bool get isEmpty => projects.iEmpty; // And projects is a List: List get projects { /* ... */ } }Then, I expect that dart_analyzer will complain:
➜ ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of List<dynamic> (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 57: 58: bool get isEmpty => projects.iEmpty; ~~~~~~Instead, I get a long list complaining about all of the JavaScript methods that were not declared to be a part of the js-interop Proxy class:
➜ ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart file:/home/chris/.pub-cache/hosted/pub.dartlang.org/js-0.0.22/lib/js.dart:78:1: dart:mirrors is not fully implemented yet 77: import 'dart:isolate'; 78: import 'dart:mirrors'; ~~~~~~~~~~~~~~~~~~~~~~ file:/home/chris/repos/ice-code-editor/lib/editor.dart:172:18: "ace" is not a member of Proxy (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 171: scripts.first.onLoad.listen((event) { 172: js.context.ace.config.set("workerPath", "packages/ice_code_editor/js/ace"); ~~~ file:/home/chris/repos/ice-code-editor/lib/editor.dart:221:40: "Proxy" has no method named "setFontSize" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 220: 221: set fontSize(String size) => $unsafe.setFontSize(size); ~~~~~~~~~~~ file:/home/chris/repos/ice-code-editor/lib/editor.dart:222:38: "Proxy" has no method named "setTheme" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 221: set fontSize(String size) => $unsafe.setFontSize(size); 222: set theme(String theme) => $unsafe.setTheme(theme); ~~~~~~~~ file:/home/chris/repos/ice-code-editor/lib/editor.dart:223:44: "Proxy" has no method named "setPrintMarginColumn" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 222: set theme(String theme) => $unsafe.setTheme(theme); 223: set printMarginColumn(bool b) => $unsafe.setPrintMarginColumn(b); ~~~~~~~~~~~~~~~~~~~~ ...(this goes on for quite some time)
So I break out
sed
to not print (!p
) lines that fall between two matches. In all, there are four different type of errors that I find that I care nothing for, so the dart_analyzer
+ sed
command becomes:➜ ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart 2>&1 | \ sed -n '/js-0.0.22\/lib\/src\/wrapping/,/~~/!p' | \ sed -n '/is not a member of Proxy/,/~~/!p' | \ sed -n '/"Proxy" has no method named/,/~~/!p' | \ sed -n '/dart:mirrors is not fully implemented yet/,/~~/!p' file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of ListThat's a lot to be typing each time, so I bundle that into an internal tool for the package, which pub package convention says goes in the(sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 57: 58: bool get isEmpty => projects.iEmpty; ~~~~~~
tools
sub-directory. So tools/js_dart_analyzer
becomes:#!/usr/bin/env sh dart_analyzer $* 2>&1 | \ sed -n '/js-0.0.22\/lib\/src\/wrapping/,/~~/!p' | \ sed -n '/is not a member of Proxy/,/~~/!p' | \ sed -n '/"Proxy" has no method named/,/~~/!p' | \ sed -n '/dart:mirrors is not fully implemented yet/,/~~/!p'With that, I have a much saner time running a js-interop safe version of the
dart_analyzer
tool:➜ ice-code-editor git:(master) ✗ ./tool/js_dart_analyzer lib/ice.dart file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of ListThe last thing that I do during this code re-organization is to put these parts to good use. While working in the(sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart) 57: 58: bool get isEmpty => projects.iEmpty; ~~~~~~
Store
class, it has become apparent that I have misplaced my Gzip'ing utilities. I had thought them intimately part of Store
, which is why I had included them in the class:part of ice; class Store implements HashMap<String, HashMap> { // ... static String encode(String string) { var gzip = js.context.RawDeflate.deflate(string); return CryptoUtils.bytesToBase64(gzip.codeUnits); } static String decode(String string) { var bytes = CryptoUtils.base64StringToBytes(string); var gzip = new String.fromCharCodes(bytes); return js.context.RawDeflate.inflate(gzip); } // ... }Now it is apparent that the Store might make use of these js-interop methods, but they really belong in a separate,
Gzip
class. And, since parts make it so darn easy to organize code, I create gzip.dart
with:part of ice; class Gzip { static String encode(String string) { var gzip = js.context.RawDeflate.deflate(string); return CryptoUtils.bytesToBase64(gzip.codeUnits); } static String decode(String string) { var bytes = CryptoUtils.base64StringToBytes(string); var gzip = new String.fromCharCodes(bytes); return js.context.RawDeflate.inflate(gzip); } }Then I can make this a part of
ice
as well:library ice; import 'dart:async'; import 'dart:collection'; import 'dart:crypto'; import 'dart:html'; import 'dart:json' as JSON; import 'package:js/js.dart' as js; import 'package:js/js_wrapping.dart' as jsw; part 'editor.dart'; part 'store.dart'; part 'gzip.dart';That seems a fine stopping point for tonight. Up tomorrow: I get back to adding features.
Day #749
No comments:
Post a Comment