效果图展示
调试页面需求分析
- 一个远程调试业务的客户端部分,代码编辑器支持实时高亮功能,语言需要支持JavaScript+Python以及meta语言。
- 子需求1:代码行数对应显示。
- 子需求2:断点功能 。(包括增加,删除,禁用,启用)
- 子需求3:选中单行功能。(点击选中某行,或者断点自动停在某行,页面自动滑动到对应位置)。
- 子需求4:快捷输入功能。(换行自动补充大括号,快捷符号输入等)
- 子需求5:远程调试流程。
功能调研
可能开始查找方式不太对,没找到什么好的解决方案,去App store上找了一圈,数量极少,且多以收费内容居多,想着应该是极小众的需求,只能自己去看编译前端知识,好在高亮功能只涉及词法分析,于是拜读《编译原理》,看token解析过程、有限状态机的工作原理。但始终对于规则制定方面很是担心,一是对正则语法不太熟悉,担心漏判,导致高亮功能不能全部覆盖所有场景,二是开发周期长,补充知识是个很漫长的过程,高亮需求本属于用户体验提升的部分,为实现这么个功能放一两个月的开发时间也是够呛。
高亮功能
方案1
- 正则匹配文本,得到 注释、字符串、数字、运算符、关键字等 (每种语言写一套)
- 依次匹配对应色值。
- 编辑时监听变化,重复执行1、2步骤。
这样最简单,但主要的缺点是不支持词和词 之间的关系 比如:
Class Student : NSObject
以上Student按理也应该变色的,但是匹配不到
方案2
Google搜到了js开源框架 Highlight.js ,支持一百多种语言,一百多种主题,市场上流行的IDE色调基本都支持,且有iOS移植框架可以直接使用,项目地址
,但悲催的是需要支持的 meta语言 是公司自己开发的,想要完美适配必须要改js源代码了,源代码500+k,并非小项目,想必改动的工作量也不少。
移植框架的工作原理为:
- 其中Highlight类,使用JavaScriptCore框架解析Highlight.js 执行js脚本将代码文本转换为html。
- 内部类Theme 解析主题对应的css文件,给NSAttributedString设置颜色字体内容
- 内部类CodeAttributeString 遍历span标签,将html转换为NSAttributedString,CodeAttributeString继承自NSTextStorage,通过重写processEditing方法实现实时编辑后的高亮显示。(非全局监听,只监听编辑所在行,很高效)
- 将CodeAttributeString设置为UITextview的textContainer属性。
self.textStorage = [[CodeAttributedString alloc] init];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[self.textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
self.textView = [[CodeTextView alloc] initWithFrame:self.view.bounds textContainer:textContainer];
- 调用setText:实现高亮显示。
断点功能
1.代码行数显示
Highlight框架居然不带行数显示,所以断点没出打,没办法必须加功能。这里有个误区就是代码行数和文字行数并不是一对一的关系,屏幕宽度不一样,代码伸展的size也不一样,所以会有一行代码对应多行文本的情况。
方案1
新建左侧NumberLineView继承自ScrollView,将屏幕空间划分为两部分,右侧textView给左侧提供行数内容,左侧跟随右侧滑动显示。
此方案太low,代码量和逻辑估计也不少,跳过。
方案2
- 使用单一UITextview实现,先设置contentInset属性将文字部分右移。
- 遍历文字range 得到行数及对应rect信息。
//这块被坑了很久,不知道有系统api已经实现,还打算逐个遍历字符根据屏幕宽度计算rect....
[self.text enumerateSubstringsInRange:NSMakeRange(0, self.text.length) options:NSStringEnumerationByParagraphs usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
NSRange range = [self.layoutManager glyphRangeForCharacterRange:substringRange actualCharacterRange:nil];
CGRect rect = [self.layoutManager boundingRectForGlyphRange:range inTextContainer:self.textContainer];
//此处 rect 就是每一行文字对应的矩形,随屏幕宽度变化而变化
}];
获取rect效果图
行数类结构
- 重写drawRect方法,将行数文字和rect绘制到左侧空余部分。
- (void)drawRect:(CGRect)rect
{
...
for (LineNumberModel *model in self.lineModelArray){
[model.lineStr drawInRect:model.rectValve.CGRectValue];
}
...
}
2.断点控制
- 增加
增加涉及到断点保存的场景,为提升效率本地和服务端都维护一份断点数组,断点模型需要的信息有:
- id //不解释
- 脚本id //收到远程断点的时候,需要展示对应脚本信息
- 行数 //用于和左侧行数栏匹配显示断点
- 状态 //用于显示断点启用状态
- 删除
调用接口,重绘UI - 禁用
调用接口,重绘UI - 启用
调用接口,重绘UI
选中单行功能
- 自动选中
监听到远程中断事件后,正常情况下需要做一系列的定位操作,包括打开页面、展示数据、获取对应脚本、定位到所在行等。
另外还需要考虑异常场景,所以开始写逻辑前,最好先多想一些测试用例,比如通过断点定位不到脚本的情况,脚本和行数对应不上的情况,或者定位过程中,用户正在操作UI时的情况。
最怕写的过程中,因为突然想起少考虑了一个重要场景,出现各种花式适配.....这种情况多了,就成了加班的噩梦。所以还不如开始就耐着性子画些脑图,流程图把自己脑子先收拾清楚再说。 -
点击选中
这个比较简单了,textView增加点击手势,遍历数组判断点击区域是否属于行数内某rect范围内,是的话就标记到drawRect里绘制。
点击选中42行
有个冲突的地方是增加tap手势以后,textView就不能响应弹出键盘了,所以还需要在非代码行数的范围内主动弹出键盘,并将光标定位在点击位置,也是现有API。
if (isNotNumberViewRect){
self.editable = YES;
[self becomeFirstResponder];
CGPoint point = [tap locationInView:tap.view];
UITextRange *range = [self characterRangeAtPoint:point];
self.selectedTextRange = range;
}
- 软键盘快捷输入
软键盘写代码主要有以下缺点:
- 不好定位光标。发现一个单词输错几个字符,要定位到错误位置简直崩溃。
解决方案,增加 ←,→快捷输入按钮,上下相对好操作些,就不加按钮了,空间有限。 -
代码用到的符号都藏得很深。比如成对的 (),[],{},"",''。
快捷输入
-'{' 符输入后换行自动补充 '}'。
逻辑原理:
- 检测\n符号
- 找出换行前最后一个非空白字符是不是左花括号:
- 插入tab并再次换行
- 补齐右花括号
- 自动缩进?
- 自动平衡花括号?
- 代码提示? 没想到什么好的方案,貌似只能本地建立个词汇库,然后通过谓词查找,成本不低。
远程调试
调试流程
- 启动调试
- 启动长连接监听远程事件。(断点、异常,或日志)
- 断点操作。(增加,删除,启用,禁用)
- 收到远程事件,定位UI。
- 点击断点下一步、继续的执行逻辑。
网友评论