diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index fa6bcb0c..b2792720 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -118,7 +118,7 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
         Align::Bottom => height,
     };
 
-    view.first_line = line.saturating_sub(relative);
+    view.offset.row = line.saturating_sub(relative);
 }
 
 /// A command is composed of a static name, and a function that takes the current state plus a count,
@@ -465,8 +465,8 @@ fn goto_window(cx: &mut Context, align: Align) {
     let last_line = view.last_line(doc);
 
     let line = match align {
-        Align::Top => (view.first_line + scrolloff),
-        Align::Center => (view.first_line + (height / 2)),
+        Align::Top => (view.offset.row + scrolloff),
+        Align::Center => (view.offset.row + (height / 2)),
         Align::Bottom => last_line.saturating_sub(scrolloff),
     }
     .min(last_line.saturating_sub(scrolloff));
@@ -892,7 +892,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
 
     let last_line = view.last_line(doc);
 
-    if direction == Backward && view.first_line == 0
+    if direction == Backward && view.offset.row == 0
         || direction == Forward && last_line == doc_last_line
     {
         return;
@@ -904,9 +904,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
         .scrolloff
         .min(view.area.height as usize / 2);
 
-    view.first_line = match direction {
-        Forward => view.first_line + offset,
-        Backward => view.first_line.saturating_sub(offset),
+    view.offset.row = match direction {
+        Forward => view.offset.row + offset,
+        Backward => view.offset.row.saturating_sub(offset),
     }
     .min(doc_last_line);
 
@@ -916,7 +916,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
     // clamp into viewport
     let line = cursor
         .row
-        .max(view.first_line + scrolloff)
+        .max(view.offset.row + scrolloff)
         .min(last_line.saturating_sub(scrolloff));
 
     let text = doc.text().slice(..);
@@ -4058,13 +4058,13 @@ fn split(cx: &mut Context, action: Action) {
     let (view, doc) = current!(cx.editor);
     let id = doc.id();
     let selection = doc.selection(view.id).clone();
-    let first_line = view.first_line;
+    let offset = view.offset;
 
     cx.editor.switch(id, action);
 
     // match the selection in the previous view
     let (view, doc) = current!(cx.editor);
-    view.first_line = first_line;
+    view.offset = offset;
     doc.set_selection(view.id, selection);
 }
 
@@ -4113,7 +4113,7 @@ fn align_view_middle(cx: &mut Context) {
         .cursor(doc.text().slice(..));
     let pos = coords_at_pos(doc.text().slice(..), pos);
 
-    view.first_col = pos.col.saturating_sub(
+    view.offset.col = pos.col.saturating_sub(
         ((view.area.width as usize).saturating_sub(crate::ui::editor::GUTTER_OFFSET as usize)) / 2,
     );
 }
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 4e01ce1c..985d4e48 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -261,7 +261,7 @@ impl Component for Completion {
                 .primary()
                 .cursor(doc.text().slice(..));
             let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
-                - view.first_line) as u16;
+                - view.offset.row) as u16;
 
             let mut doc = match &option.documentation {
                 Some(lsp::Documentation::String(contents))
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 5e1e8f50..98462e26 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -80,10 +80,9 @@ impl EditorView {
             view.area.width - GUTTER_OFFSET,
             view.area.height.saturating_sub(1),
         ); // - 1 for statusline
-        let offset = Position::new(view.first_line, view.first_col);
         let height = view.area.height.saturating_sub(1); // - 1 for statusline
 
-        let highlights = Self::doc_syntax_highlights(doc, offset, height, theme, loader);
+        let highlights = Self::doc_syntax_highlights(doc, view.offset, height, theme, loader);
         let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
         let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
             Box::new(syntax::merge(
@@ -94,7 +93,7 @@ impl EditorView {
             Box::new(highlights)
         };
 
-        Self::render_text_highlights(doc, offset, area, surface, theme, highlights);
+        Self::render_text_highlights(doc, view.offset, area, surface, theme, highlights);
         Self::render_gutter(doc, view, area, surface, theme, config);
 
         if is_focused {
@@ -382,7 +381,7 @@ impl EditorView {
         let selection = doc.selection(view.id);
         let last_line = view.last_line(doc);
         let screen = {
-            let start = text.line_to_char(view.first_line);
+            let start = text.line_to_char(view.offset.row);
             let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
             Range::new(start, end)
         };
@@ -408,7 +407,7 @@ impl EditorView {
             );
             if let Some(head) = head {
                 // Highlight line number for selected lines.
-                let line_number = view.first_line + head.row;
+                let line_number = view.offset.row + head.row;
                 let line_number_text = if line_number == last_line && !draw_last {
                     "    ~".into()
                 } else {
@@ -435,8 +434,8 @@ impl EditorView {
 
                     if let Some(pos) = pos {
                         // ensure col is on screen
-                        if (pos.col as u16) < viewport.width + view.first_col as u16
-                            && pos.col >= view.first_col
+                        if (pos.col as u16) < viewport.width + view.offset.col as u16
+                            && pos.col >= view.offset.col
                         {
                             let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
                                 Style::default()
@@ -479,7 +478,7 @@ impl EditorView {
         let current_line = doc
             .text()
             .char_to_line(doc.selection(view.id).primary().anchor);
-        for (i, line) in (view.first_line..(last_line + 1)).enumerate() {
+        for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
             use helix_core::diagnostic::Severity;
             if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
                 surface.set_stringn(
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 32f5fe86..478f3818 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -180,7 +180,7 @@ impl Editor {
                 view.jumps.push(jump);
                 view.last_accessed_doc = Some(view.doc);
                 view.doc = id;
-                view.first_line = 0;
+                view.offset = Position::default();
 
                 let (view, doc) = current!(self);
 
@@ -194,7 +194,7 @@ impl Editor {
                     .primary()
                     .cursor(doc.text().slice(..));
                 let line = doc.text().char_to_line(pos);
-                view.first_line = line.saturating_sub(view.area.height as usize / 2);
+                view.offset.row = line.saturating_sub(view.area.height as usize / 2);
 
                 return;
             }
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index c7309fe9..f688dd7f 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -61,8 +61,7 @@ impl JumpList {
 pub struct View {
     pub id: ViewId,
     pub doc: DocumentId,
-    pub first_line: usize,
-    pub first_col: usize,
+    pub offset: Position,
     pub area: Rect,
     pub jumps: JumpList,
     /// the last accessed file before the current one
@@ -74,8 +73,7 @@ impl View {
         Self {
             id: ViewId::default(),
             doc,
-            first_line: 0,
-            first_col: 0,
+            offset: Position::new(0, 0),
             area: Rect::default(), // will get calculated upon inserting into tree
             jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
             last_accessed_doc: None,
@@ -91,7 +89,7 @@ impl View {
         let line = pos.row;
         let col = pos.col;
         let height = self.area.height.saturating_sub(1); // - 1 for statusline
-        let last_line = (self.first_line + height as usize).saturating_sub(1);
+        let last_line = (self.offset.row + height as usize).saturating_sub(1);
 
         // - 1 so we have at least one gap in the middle.
         // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
@@ -100,22 +98,22 @@ impl View {
 
         // TODO: not ideal
         const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
-        let last_col = (self.first_col + self.area.width as usize).saturating_sub(OFFSET + 1);
+        let last_col = (self.offset.col + self.area.width as usize).saturating_sub(OFFSET + 1);
 
         if line > last_line.saturating_sub(scrolloff) {
             // scroll down
-            self.first_line += line - (last_line.saturating_sub(scrolloff));
-        } else if line < self.first_line + scrolloff {
+            self.offset.row += line - (last_line.saturating_sub(scrolloff));
+        } else if line < self.offset.row + scrolloff {
             // scroll up
-            self.first_line = line.saturating_sub(scrolloff);
+            self.offset.row = line.saturating_sub(scrolloff);
         }
 
         if col > last_col.saturating_sub(scrolloff) {
             // scroll right
-            self.first_col += col - (last_col.saturating_sub(scrolloff));
-        } else if col < self.first_col + scrolloff {
+            self.offset.col += col - (last_col.saturating_sub(scrolloff));
+        } else if col < self.offset.col + scrolloff {
             // scroll left
-            self.first_col = col.saturating_sub(scrolloff);
+            self.offset.col = col.saturating_sub(scrolloff);
         }
     }
 
@@ -125,7 +123,7 @@ impl View {
         let height = self.area.height.saturating_sub(1); // - 1 for statusline
         std::cmp::min(
             // Saturating subs to make it inclusive zero indexing.
-            (self.first_line + height as usize).saturating_sub(1),
+            (self.offset.row + height as usize).saturating_sub(1),
             doc.text().len_lines().saturating_sub(1),
         )
     }
@@ -141,7 +139,7 @@ impl View {
     ) -> Option<Position> {
         let line = text.char_to_line(pos);
 
-        if line < self.first_line || line > self.last_line(doc) {
+        if line < self.offset.row || line > self.last_line(doc) {
             // Line is not visible on screen
             return None;
         }
@@ -161,8 +159,8 @@ impl View {
         }
 
         // It is possible for underflow to occur if the buffer length is larger than the terminal width.
-        let row = line.saturating_sub(self.first_line);
-        let col = col.saturating_sub(self.first_col);
+        let row = line.saturating_sub(self.offset.row);
+        let col = col.saturating_sub(self.offset.col);
 
         Some(Position::new(row, col))
     }
@@ -186,7 +184,7 @@ impl View {
             return None;
         }
 
-        let line_number = (row - self.area.y) as usize + self.first_line;
+        let line_number = (row - self.area.y) as usize + self.offset.row;
 
         if line_number > text.len_lines() - 1 {
             return Some(text.len_chars());
@@ -196,7 +194,7 @@ impl View {
 
         let current_line = text.line(line_number);
 
-        let target = (column - OFFSET - self.area.x) as usize + self.first_col;
+        let target = (column - OFFSET - self.area.x) as usize + self.offset.col;
         let mut selected = 0;
 
         for grapheme in RopeGraphemes::new(current_line) {