monaco-editor实现全局内容和文件搜索

monaco-editor不提供全局搜索,只提供单个文件内的搜索,那么如何实现全局搜索呢?

环境:Nodejs + React + Dva + monaco-editor + react-monaco-editor + antd

一、绑定快捷键

调用editor.addCommand方法绑定快捷键,通过monaco.KeyMod和monaco.KeyCode选择快捷键

快捷键要在编辑器创建完成时创建,故这段代码要写在editorDidMount里面

全局搜索要有一个搜索框供输入关键字,故在触发快捷键的时候让搜索框显示,默认是不显示。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
editorDidMount = (editor, monaco) => {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_F, () => {
// 显现全局搜索框
this.setState({
showModel: true
})
});

editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_N, () => {
// 显现文件搜索框
this.setState({
showFileModel: true
})
});
editor.focus();
}

二、搜索框实现

搜索框样式主要看自己的需求,我这里只写个简单的搜索框

如何实现搜索框根据内容多少滚动显示列表是我们要考虑的一个问题,经过一番搜索,我锁定了react-infinite-scroller,通过InfiniteScroll API实现滚动下拉效果。代码如下:

1
2
3
4
5
6
7
8
9
10
11
<div style={{height:'400px', overflow:'auto'}}>
<InfiniteScroll
pageStart={0}
loadMore={true}
hasMore={true || false}
loader={null}
useWindow={false}
>
{rowsList}
</InfiniteScroll>
</div>

三、关键字搜索功能

monaco-editor不提供全局搜索功能,如果要在monaco-editor基础上实现全局搜索,我们可以借助单个文件的搜索功能,然后遍历业务系统的所有文件,对每个文件的搜索结果进行汇总实现全局搜索。

这里需要考虑几个问题:

(1) monaco-editor单个文件搜索是基于创建model的基础上,而通过编辑器点击打开某个文件时才会创建model,我们怎么能获取业务系统所有文件的model?
(2) 业务系统有.git,node_modules等不需要遍历的文件,还有文件夹里的子文件夹及子文件,那么如何实现遍历?

首先针对第一个问题做出解答:

如果对monaco-editor没有经过一番研究可能不会发现这个规律,通过查看官网的api,发现monaco-editor提供createModel方法,我们可以手动创建model

针对第二个问题做出解答

其实.git ,node_modules文件很好排除,只要遍历的时候加个if判断就行;

遍历到文件夹时,我们可以通过nodejs提供的fs.Stats类里面的isDirectory()方法判断,如果是文件夹,就进入文件夹里继续遍历,这里用到递归遍历

在单个文件中搜索关键字可以使用model.findMatches(keyword)实现

再建一个全局的变量用于保存遍历到的结果就行。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
let result =  new Map();

/**
* 业务系统文件遍历
* @param projectPath
* @param searchText
*/
getProjectFileListInfo = (projectPath, searchText) => {

let pa = window.fs.readdirSync(projectPath);
let that = this;
pa.forEach(function(ele,index){
let info = window.fs.statSync(projectPath+"/"+ele);
if(ele == 'node_modules' || ele == '.git' ) {

} else if(info.isDirectory()){
that.getProjectFileListInfo(projectPath + "/"+ ele , searchText)

}else{
// 读取文件内容
const fileInfo = window.fs.readFileSync(projectPath+'/'+ele, 'utf-8');
// 根据文件名后缀判断应该创建哪种model
// 分离后缀
let suffix = ele.split('.')[1];
switch (suffix) {
case 'js':

let tempModel1 = monaco.editor.createModel(fileInfo, 'javascript');
that.findMatches(tempModel1, searchText, projectPath + "/"+ ele, ele);
break;
case 'html':

let tempModel2 = monaco.editor.createModel(fileInfo, 'html');
that.findMatches(tempModel2, searchText, projectPath + "/"+ ele, ele);
break;
case 'json':

let tempModel3 = monaco.editor.createModel(fileInfo, 'javascript');
that.findMatches(tempModel3, searchText, projectPath + "/"+ ele, ele);
break;
case 'java':

let tempModel4 = monaco.editor.createModel(fileInfo, 'java');
that.findMatches(tempModel4, searchText, projectPath + "/"+ ele, ele);
break;
}

}
})

};


/**
* 根据关键字匹配单个文件
* @param model
* @param searchText
* @param filePath
* @param fileName
*/
findMatches = (model, searchText, filePath, fileName) => {
let arr = [];
for (let match of model.findMatches(searchText)) {
arr.push({
text:model.getLineContent(match.range.startLineNumber),
range: match.range,
model: model,
filePath: filePath,
fileName: fileName
});
}

result.set(model.uri.toString(),arr);
};

四、搜索结果列表展示

先看效果图

在展示列表中,我们希望看到关键字所在的文件名,行号,甚至是路径,只要我们保存展示结果时将这些信息都保存进去,然后react渲染时渲染出来,代码如上面arr变量所示

五、关键字高亮显示

在上图我们可以看到展示结果中所有的登录关键字都是黄色背景显示的,这里调用antd的Typography 实现高亮。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ev为result,全局搜索的结果
let i=0;
let rowsList = [];
ev.forEach((values,key) =>{
values.forEach((row) => {
i++;
let rowReplace = row.text.replace(keyword,'<Text mark>'+keyword+'</Text>');
// 关键字字符串截取,添加高亮
let preV = row.text.substring(0,row.range.startColumn - 1);
let nextV = row.text.substring(row.range.endColumn - 1, row.text.length);
rowsList.push(<List.Item className="SelectInfo-listItem" key={i} onClick={() => goto(row.range, row.model, row.filePath, row.fileName)}>
<div>{preV}<Text mark>{keyword}</Text>{nextV}</div>
<List.Item.Meta/>
<div className="SelectInfo-foot">{row.fileName}: {row.range.startLineNumber}</div>
</List.Item>);
})
});

从代码中可以看出,我这里使用的是字符串截取找到关键字然后给关键字添加。可以看出我给展示添加了文件名和所在行属性。

六、goto点击跳转

实现差不多了,怎么能少掉点击展示列跳转到对应文件即编辑器打开对应文件

从标题五可以看出,我给每一行加了onClick点击调用goto方法。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//这里range和model,对应findAllMatches返回结果集合里面对象的range和model属性
goto = (range, model, filePath, fileName) => {
this.openFileEditor(filePath, fileName);
this.setState({
showModel: false
});
this.setState({
showFileModel: false
})

};

openFileEditor = (file, fileName) => {

const { dispatch } = this.props;
dispatch({
type: 'project/openTab',
payload: {
key: fileName, title: fileName, model: { filePath: file, fileName: fileName }
}
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

/**
* TAB:{
* key:'', 标签页唯一标识
* title:'', 标签页标题
* isDirty: false, 标识模型数据是否发生变化, 关闭标签页前判断isDirty是否true,如果是,则需要触发保存的方法
* model: {
* }
* }
* @param {*} state
* @param {*} action
*/
openTab(state, {payload}) {
let openTabs = state.openTabs.concat([]);
const openFile = {
key: payload.key,
title: payload.title,
extension: payload.extension,
model: payload.model
};

openFile.isDirty = false;
const index = openTabs.findIndex(file => file.key == openFile.key);
if (index == -1) {
openTabs.push(openFile);
}
return {...state, openTabs, selectTabPane: openFile.key, lastSelectedPane: state.selectTabPane}
},
// 通过openTab收集所有打开的文件保存在openTabs里,方便后续的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 打开的文件
*/
let openTabs = this.props.openTabs.map(tab => {
const title = (tab.isDirty ? "* " : '' ) + tab.title;
return <TabPane style={{height: '100%'}} closable={true} tab={title} key={ tab.key }> {pluginInject.openDynamicTab(tab)} </TabPane>
});
panels = panels.concat(openTabs);

/**
通过{
panels
}将面板显示出来
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 打开标签页面板
*/
openDynamicTab(tab) {
const { key } = tab;
//如果传入的参数没有extension, 则通过key取得
const extension = tab.extension ? tab.extension : key.split('.')[1];
const editor = this.getEditorPlugin(extension);
if (editor) {
return this.getEditorPlugin(extension).getTabPanel(tab, extension);
}
return <div>未找到[{extension}]类型的编辑器</div>
},

七 全局文件搜索

文件搜索比关键字搜索简单一点,在遍历业务系统时遍历所有文件,将和关键字匹配的所有文件保存到result中, 其他的操作一样

结束

部分源码请到Github上:https://github.com/griabcrh/global-search

到这里就结束了,恭喜你学会了怎么实现全局搜索,如果觉得本问对你有帮助,欢迎关注公众号:java开发高级进阶,get到跟多知识。