diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index cc4fab05..69c7ad58 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -351,6 +351,69 @@ pub fn split_selection_on_newline(cx: &mut Context) {
     doc.set_selection(selection);
 }
 
+// search: searches for the first occurence in file, provides a prompt
+// search_next: reuses the last search regex and searches for the next match. The next match becomes the main selection.
+// -> we always search from after the cursor.head
+// TODO: be able to use selection as search query (*/alt *)
+// I'd probably collect all the matches right now and store the current index. The cache needs
+// wiping if input happens.
+
+fn _search(doc: &mut Document, contents: &str, regex: &Regex) {
+    let text = doc.text();
+    let start = doc.selection().cursor();
+
+    // TODO: use find_at to find the next match after the cursor, loop around the end
+    if let Some(mat) = regex.find_at(&contents, start) {
+        let start = text.byte_to_char(mat.start());
+        let end = text.byte_to_char(mat.end());
+        let selection = Selection::single(start, end - 1);
+        // TODO: (first_match, regex) stuff in register?
+        doc.set_selection(selection);
+    };
+}
+
+// TODO: use one function for search vs extend
+pub fn search(cx: &mut Context) {
+    let doc = cx.doc();
+
+    // TODO: could probably share with select_on_matches?
+
+    // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
+    // feed chunks into the regex yet
+    let contents = doc.text().slice(..).to_string();
+
+    let prompt = ui::regex_prompt(cx, "search:".to_string(), move |doc, regex| {
+        let text = doc.text();
+        let start = doc.selection().cursor();
+        _search(doc, &contents, &regex);
+
+        // TODO: only store on enter (accept), not update
+        register::set('\\', vec![regex.as_str().to_string()]);
+    });
+
+    cx.callback = Some(Box::new(
+        move |compositor: &mut Compositor, editor: &mut Editor| {
+            compositor.push(Box::new(prompt));
+        },
+    ));
+}
+
+pub fn search_next(cx: &mut Context) {
+    if let Some(query) = register::get('\\') {
+        let query = query.first().unwrap();
+        let doc = cx.doc();
+        let contents = doc.text().slice(..).to_string();
+        let regex = Regex::new(&query).unwrap();
+        _search(doc, &contents, &regex);
+    }
+}
+
+// TODO: N -> search_prev
+// need to loop around buffer also and show a message
+// same for no matches
+
+//
+
 pub fn select_line(cx: &mut Context) {
     // TODO: count
     let pos = cx.doc().selection().primary();
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index dbf3459f..932a6431 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -168,6 +168,8 @@ pub fn default() -> Keymaps {
                 vec![alt!(';')] => commands::flip_selections,
                 vec![key!('%')] => commands::select_all,
                 vec![key!('x')] => commands::select_line,
+                vec![key!('/')] => commands::search,
+                vec![key!('n')] => commands::search_next,
                 vec![key!('u')] => commands::undo,
                 vec![shift!('U')] => commands::redo,
                 vec![key!('y')] => commands::yank,
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 29483705..e1899144 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -43,12 +43,15 @@ pub fn regex_prompt(
                 PromptEvent::Update => {
                     match Regex::new(input) {
                         Ok(regex) => {
-                            let doc = &mut editor.view_mut().doc;
+                            let view = &mut editor.view_mut();
+                            let doc = &mut view.doc;
 
                             // revert state to what it was before the last update
                             doc.state = snapshot.clone();
 
                             fun(doc, regex);
+
+                            view.ensure_cursor_in_view();
                         }
                         Err(_err) => (), // TODO: mark command line as error
                     }