diff --git a/docs/manual/admin/configuration/diffviewer-settings.rst b/docs/manual/admin/configuration/diffviewer-settings.rst
index e7989733fd0eb5254d75c4a2f2755d07ba04c906..8f47ebe3cef2c38d9cbf1ab8ee586c451b14b9bf 100644
--- a/docs/manual/admin/configuration/diffviewer-settings.rst
+++ b/docs/manual/admin/configuration/diffviewer-settings.rst
@@ -9,12 +9,21 @@ diff viewer. These settings generally don't need to be changed unless you have
 specified requirements for your server. It's split up into the following
 sections:
 
-* `General`_
+* `Appearance`_
+* `Limits`_
+* `Code Safety`_
 * `Advanced`_
 
 
-General
-=======
+Appearance
+==========
+
+* **Show trailing whitespace:**
+    If enabled, excess whitespace on a line is shown as red blocks. This
+    helps to visualize when a text editor has added unwanted whitespace to the
+    end of a line.
+
+    This defaults to being enabled.
 
 * **Show syntax highlighting:**
     If enabled, syntax highlighting will be used in the Diff Viewer. This
@@ -28,25 +37,64 @@ General
 
     This defaults to being enabled.
 
-* **Syntax highlighting threshold:**
+* **Custom file highlighting:**
+    This is a mapping of file extensions to Pygments lexers. This is used
+    to customize the type of syntax highlighting that gets applied to files.
+
+    This defaults to mapping ``.less`` files to the ``LessCss`` lexer.
+
+* **Tabstop size:**
+    The default character width to use for tab characters. If unset, tabs will
+    show as 8 characters wide by default.
+
+
+Limits
+======
+
+* **Max diff size in bytes:**
+    The maximum size of an uploaded diff (in bytes).
+
+    Any diffs larger than this will be rejected during upload. This can be
+    used to lighten the load on the server.
+
+    Specify 0 to allow diffs of any size.
+
+    This defaults to 2097152 (2MB).
+
+* **Max size for binary files in diffs:**
+    The maximum size of binary files to include in diffs (in bytes).
+
+    For binary file types which can be shown inline in the diff (for example,
+    images), this is the limit for how large those files can be.
+
+    This defaults to 10485760 (10MB).
+
+* **Max lines for syntax highlighting:**
     This can be used to limit syntax highlighting for very large files. If
     this isn't blank or ``0``, then syntax highlighting will only be enabled
     for files with at most this many lines.
 
     This defaults to being blank.
 
-* **Mapping of file extensions to syntax highlighters:**
-    This is a mapping of file extensions to Pygments lexers. This is used
-    to customize the type of syntax highlighting that gets applied to files.
 
-    This defaults to mapping ``.less`` files to the ``LessCss`` lexer.
+Code Safety
+===========
 
-* **Show trailing whitespace:**
-    If enabled, excess whitespace on a line is shown as red blocks. This
-    helps to visualize when a text editor has added unwanted whitespace to the
-    end of a line.
+* **Check for potentially misleading Unicode characters:**
+    Review Board checks code for suspicious characters used in `Trojan Source`_
+    attacks. Uncheck this box to disable detection of Unicode "confusables".
 
-    This defaults to being enabled.
+* **Safe character sets:**
+    If you have code or comments that uses languages which include characters
+    commonly used for trojan source attacks, you can disable detection of those
+    by marking those languages as safe.
+
+
+.. _Trojan Source: https://trojansource.codes/
+
+
+Advanced
+========
 
 * **Show all whitespace for:**
     This is a comma-separated list of file patterns for which all whitespace
@@ -59,20 +107,6 @@ General
 
     For example: ``*.py, *.txt``
 
-
-Advanced
-========
-
-* **Max diff size:**
-    The maximum size of an uploaded diff (in bytes).
-
-    Any diffs larger than this will be rejected during upload. This can be
-    used to lighten the load on the server.
-
-    Specify 0 to allow diffs of any size.
-
-    This defaults to 0.
-
 * **Lines of Context:**
     The number of unchanged lines shown above and below changed lines.
 
diff --git a/reviewboard/admin/forms/diff_settings.py b/reviewboard/admin/forms/diff_settings.py
index f649bce339a3bf68f95c27e268e47e18604498f2..83d75736cc55e25963d7285b784aa076da1ed816 100644
--- a/reviewboard/admin/forms/diff_settings.py
+++ b/reviewboard/admin/forms/diff_settings.py
@@ -48,6 +48,16 @@ class DiffSettingsForm(SiteSettingsForm):
         required=False,
         widget=ListEditWidget(value_widget=LexersMappingWidget))
 
+    diffviewer_default_tab_size = forms.IntegerField(
+        label=_('Tabstop size'),
+        help_text=_(
+            'Set this to override the default character width for tabstops. '
+            'If unset or set to 0, tabstops will show as 8 characters wide.'
+        ),
+        required=False,
+        widget=forms.TextInput(attrs={'size': '2'}))
+
+
     diffviewer_show_trailing_whitespace = forms.BooleanField(
         label=_('Show trailing whitespace'),
         help_text=_('Show excess trailing whitespace as red blocks. This '
@@ -216,6 +226,7 @@ class DiffSettingsForm(SiteSettingsForm):
                     'diffviewer_show_trailing_whitespace',
                     'diffviewer_syntax_highlighting',
                     'diffviewer_custom_pygments_lexers',
+                    'diffviewer_default_tab_size',
                 ),
             },
             {
@@ -237,7 +248,7 @@ class DiffSettingsForm(SiteSettingsForm):
                     'Review Board by default checks code for suspicious '
                     'Unicode characters used in '
                     '<a href="https://trojansource.codes/">Trojan Source</a> '
-                    'attacks. These checks can be fine-tunes to avoid '
+                    'attacks. These checks can be fine-tuned to avoid '
                     'matching characters in some languages, at the expense '
                     'of decreased code safety.'
                 ),
diff --git a/reviewboard/admin/siteconfig.py b/reviewboard/admin/siteconfig.py
index 55d41f20368bbe5f5d6c74b70089d894fc756b13..1b601741719943a6400e205d41c892aca6b2a106 100644
--- a/reviewboard/admin/siteconfig.py
+++ b/reviewboard/admin/siteconfig.py
@@ -24,6 +24,7 @@ from reviewboard.accounts.backends import auth_backends
 from reviewboard.accounts.privacy import recompute_privacy_consents
 from reviewboard.accounts.sso.backends import sso_backends
 from reviewboard.avatars import avatar_services
+from reviewboard.diffviewer.settings import DiffSettings
 from reviewboard.oauth.features import oauth2_service_feature
 from reviewboard.notifications.email.message import EmailMessage
 from reviewboard.search.search_backends.whoosh import WhooshBackend
@@ -126,6 +127,7 @@ defaults.update({
     'default_ui_theme': 'default',
     'default_use_rich_text': True,
     'diffviewer_context_num_lines': 5,
+    'diffviewer_default_tab_size': DiffSettings.DEFAULT_TAB_SIZE,
     'diffviewer_include_space_patterns': [],
     'diffviewer_max_binary_size': 10_485_760,
     'diffviewer_max_diff_size': 2_097_152,
diff --git a/reviewboard/diffviewer/chunk_generator.py b/reviewboard/diffviewer/chunk_generator.py
index 43029016cc589385a31c9d30bfe4c21af2e584bc..73731005d2cb705f1fbe148c861217f3da5d7e08 100644
--- a/reviewboard/diffviewer/chunk_generator.py
+++ b/reviewboard/diffviewer/chunk_generator.py
@@ -1,9 +1,13 @@
+"""Diff chunk generator implementations."""
+
+from __future__ import annotations
+
 import fnmatch
 import hashlib
 import logging
 import re
 from itertools import zip_longest
-from typing import List
+from typing import Optional, Sequence, TYPE_CHECKING, Union
 
 import pygments.util
 from django.utils.encoding import force_str
@@ -28,10 +32,14 @@ from reviewboard.diffviewer.diffutils import (get_filediff_encodings,
                                               get_patched_file,
                                               convert_to_unicode,
                                               split_line_endings)
-from reviewboard.diffviewer.opcode_generator import (DiffOpcodeGenerator,
-                                                     get_diff_opcode_generator)
+from reviewboard.diffviewer.opcode_generator import get_diff_opcode_generator
 from reviewboard.diffviewer.settings import DiffSettings
 
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
+    from reviewboard.diffviewer.models.filediff import FileDiff
+
 
 logger = logging.getLogger(__name__)
 
@@ -105,8 +113,8 @@ class RawDiffChunkGenerator(object):
         '.txt',  # ResourceLexer is used as a default.
     )
 
-    # Default tab size used in browsers.
-    TAB_SIZE = DiffOpcodeGenerator.TAB_SIZE
+    #: The default width for a tabstop.
+    TAB_SIZE = DiffSettings.DEFAULT_TAB_SIZE
 
     ######################
     # Instance variables #
@@ -121,15 +129,17 @@ class RawDiffChunkGenerator(object):
     #:     reviewboard.diffviewer.settings.DiffSettings
     diff_settings: DiffSettings
 
-    def __init__(self,
-                 old,
-                 new,
-                 orig_filename,
-                 modified_filename,
-                 encoding_list=None,
-                 diff_compat=DiffCompatVersion.DEFAULT,
-                 *,
-                 diff_settings: DiffSettings):
+    def __init__(
+        self,
+        old: Optional[Union[bytes, Sequence[bytes]]],
+        new: Optional[Union[bytes, Sequence[bytes]]],
+        orig_filename: str,
+        modified_filename: str,
+        encoding_list: Optional[Sequence[str]] = None,
+        diff_compat: int = DiffCompatVersion.DEFAULT,
+        *,
+        diff_settings: DiffSettings,
+    ) -> None:
         """Initialize the chunk generator.
 
         Version Changed:
@@ -198,6 +208,9 @@ class RawDiffChunkGenerator(object):
                 _('%s expects a Unicode value for "modified_filename"')
                 % type(self).__name__)
 
+        assert diff_settings.tab_size
+        self.diff_settings = diff_settings
+
         self.old = old
         self.new = new
         self.orig_filename = orig_filename
@@ -868,17 +881,20 @@ class RawDiffChunkGenerator(object):
         i = 0
         j = 0
 
+        tab_size = self.diff_settings.tab_size
+        assert tab_size
+
         for j, c in enumerate(chars):
             if c == ' ':
                 s += '&gt;'
                 i += 1
             elif c == '\t':
                 # Build "------>|" with the room we have available.
-                in_tab_pos = i % self.TAB_SIZE
+                in_tab_pos = i % tab_size
 
-                if in_tab_pos < self.TAB_SIZE - 1:
-                    if in_tab_pos < self.TAB_SIZE - 2:
-                        num_dashes = (self.TAB_SIZE - 2 - in_tab_pos)
+                if in_tab_pos < tab_size - 1:
+                    if in_tab_pos < tab_size - 2:
+                        num_dashes = (tab_size - 2 - in_tab_pos)
                         s += '&mdash;' * num_dashes
                         i += num_dashes
 
@@ -921,23 +937,26 @@ class RawDiffChunkGenerator(object):
         i = 0
         j = 0
 
+        tab_size = self.diff_settings.tab_size
+        assert tab_size
+
         for j, c in enumerate(chars):
             if c == ' ':
                 s += '&lt;'
                 i += 1
             elif c == '\t':
                 # Build "|<------" with the room we have available.
-                in_tab_pos = i % self.TAB_SIZE
+                in_tab_pos = i % tab_size
 
                 s += '|'
                 i += 1
 
-                if in_tab_pos < self.TAB_SIZE - 1:
+                if in_tab_pos < tab_size - 1:
                     s += '&lt;'
                     i += 1
 
-                    if in_tab_pos < self.TAB_SIZE - 2:
-                        num_dashes = (self.TAB_SIZE - 2 - in_tab_pos)
+                    if in_tab_pos < tab_size - 2:
+                        num_dashes = (tab_size - 2 - in_tab_pos)
                         s += '&mdash;' * num_dashes
                         i += num_dashes
 
@@ -1121,14 +1140,16 @@ class DiffChunkGenerator(RawDiffChunkGenerator):
     """
 
     @deprecate_non_keyword_only_args(RemovedInReviewBoard80Warning)
-    def __init__(self,
-                 request,
-                 filediff,
-                 interfilediff=None,
-                 force_interdiff=False,
-                 base_filediff=None,
-                 *,
-                 diff_settings):
+    def __init__(
+        self,
+        request: HttpRequest,
+        filediff: FileDiff,
+        interfilediff: Optional[FileDiff] = None,
+        force_interdiff: bool = False,
+        base_filediff: Optional[FileDiff] = None,
+        *,
+        diff_settings: DiffSettings,
+    ) -> None:
         """Initialize the DiffChunkGenerator.
 
         Version Changed:
@@ -1177,7 +1198,7 @@ class DiffChunkGenerator(RawDiffChunkGenerator):
         else:
             orig_filename = filediff.source_file
 
-        super(DiffChunkGenerator, self).__init__(
+        super().__init__(
             old=None,
             new=None,
             orig_filename=orig_filename,
@@ -1193,7 +1214,7 @@ class DiffChunkGenerator(RawDiffChunkGenerator):
             str:
             The new cache key.
         """
-        key: List[str] = []
+        key: list[str] = []
 
         key.append('diff-sidebyside')
 
@@ -1225,7 +1246,8 @@ class DiffChunkGenerator(RawDiffChunkGenerator):
             interdiff = None
 
         return get_diff_opcode_generator(self.differ, diff, interdiff,
-                                         request=self.request)
+                                         request=self.request,
+                                         diff_settings=self.diff_settings)
 
     def get_chunks(self):
         """Return the chunks for the given diff information.
@@ -1517,21 +1539,45 @@ def compute_chunk_last_header(lines, numlines, meta, last_header=None):
     return last_header
 
 
-_generator = DiffChunkGenerator
+_generator: type[DiffChunkGenerator] = DiffChunkGenerator
 
 
-def get_diff_chunk_generator_class():
-    """Returns the DiffChunkGenerator class used for generating chunks."""
+def get_diff_chunk_generator_class() -> type[DiffChunkGenerator]:
+    """Return the DiffChunkGenerator class used for generating chunks.
+
+    Returns:
+        type:
+        The class for the DiffChunkGenerator to use.
+    """
     return _generator
 
 
-def set_diff_chunk_generator_class(renderer):
-    """Sets the DiffChunkGenerator class used for generating chunks."""
+def set_diff_chunk_generator_class(
+    renderer: type[DiffChunkGenerator],
+) -> None:
+    """Set the DiffChunkGenerator class used for generating chunks.
+
+    Args:
+        renderer (type):
+            The class for the DiffChunkGenerator to use.
+    """
     assert renderer
 
     globals()['_generator'] = renderer
 
 
-def get_diff_chunk_generator(*args, **kwargs):
-    """Returns a DiffChunkGenerator instance used for generating chunks."""
+def get_diff_chunk_generator(*args, **kwargs) -> DiffChunkGenerator:
+    """Return a DiffChunkGenerator instance used for generating chunks.
+
+    Args:
+        *args (tuple):
+            Positional arguments to pass to the DiffChunkGenerator.
+
+        **kwargs (dict):
+            Keyword arguments to pass to the DiffChunkGenerator.
+
+    Returns:
+        DiffChunkGenerator:
+        The chunk generator instance.
+    """
     return _generator(*args, **kwargs)
diff --git a/reviewboard/diffviewer/diffutils.py b/reviewboard/diffviewer/diffutils.py
index f61593134fe2ddc035fa4425d031b1d552e37213..fa1cb6f47f50d88c03ba17d5c24ed0231b2dfae3 100644
--- a/reviewboard/diffviewer/diffutils.py
+++ b/reviewboard/diffviewer/diffutils.py
@@ -23,7 +23,8 @@ from housekeeping.functions import deprecate_non_keyword_only_args
 from typing_extensions import NotRequired, TypedDict
 
 from reviewboard.attachments.mimetypes import guess_mimetype
-from reviewboard.deprecation import RemovedInReviewBoard80Warning
+from reviewboard.deprecation import (RemovedInReviewBoard80Warning,
+                                     RemovedInReviewBoard90Warning)
 from reviewboard.diffviewer.commit_utils import exclude_ancestor_filediffs
 from reviewboard.diffviewer.errors import DiffTooBigError, PatchError
 from reviewboard.diffviewer.filetypes import (HEADER_EXTENSIONS,
@@ -63,6 +64,17 @@ _PATCH_GARBAGE_INPUT = 'patch: **** Only garbage was found in the patch input.'
 _T = TypeVar('_T')
 
 
+class DiffFileExtraContext(TypedDict):
+    """Extra context for diff files.
+
+    Version Added:
+        7.0.4
+    """
+
+    #: The tabstop width for a given file.
+    tab_size: NotRequired[int]
+
+
 class SerializedDiffFile(TypedDict):
     """Serialized information on a diff file.
 
@@ -85,6 +97,12 @@ class SerializedDiffFile(TypedDict):
     #: Whether the file was deleted.
     deleted: bool
 
+    #: Extra information about the diff file.
+    #:
+    #: Version Added:
+    #:     7.0.4
+    extra: DiffFileExtraContext
+
     #: The FileDiff for the file.
     filediff: FileDiff
 
@@ -1090,7 +1108,9 @@ def get_filediffs_match(filediff1, filediff2):
               filediff1.patched_sha1 == filediff2.patched_sha1)))
 
 
+@deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
 def get_diff_files(
+    *,
     diffset: DiffSet,
     filediff: Optional[FileDiff] = None,
     interdiffset: Optional[DiffSet] = None,
@@ -1100,6 +1120,7 @@ def get_diff_files(
     filename_patterns: Optional[list[str]] = None,
     base_commit: Optional[DiffCommit] = None,
     tip_commit: Optional[DiffCommit] = None,
+    diff_settings: Optional[DiffSettings] = None,
 ) -> list[SerializedDiffFile]:
     """Return a list of files that will be displayed in a diff.
 
@@ -1112,6 +1133,11 @@ def get_diff_files(
     This can be used along with :py:func:`populate_diff_chunks` to build a full
     list containing all diff chunks used for rendering a side-by-side diff.
 
+    Version Changed:
+        7.0.4:
+        * Made arguments keyword-only.
+        * Added the ``diff_settings`` argument.
+
     Args:
         diffset (reviewboard.diffviewer.models.diffset.DiffSet):
             The diffset containing the files to return.
@@ -1165,6 +1191,13 @@ def get_diff_files(
             :py:class:`DiffCommits <reviewboard.diffviewer.models.diffcommit
             .DiffCommit>`.
 
+        diff_settings (reviewboard.diffviewer.settings.DiffSettings, optional):
+            The diff settings object. This will become mandatory in Review
+            Board 9.0.
+
+            Version Added:
+                7.0.4
+
     Returns:
         list of dict:
         A list of dictionaries containing information on the files to show
@@ -1175,6 +1208,12 @@ def get_diff_files(
     assert not interdiffset or (base_commit is None and tip_commit is None)
     assert base_filediff is None or interfilediff is None
 
+    if diff_settings is None:
+        RemovedInReviewBoard90Warning.warn(
+            'get_diff_files was called without a diff_settings argument. '
+            'This argument will become mandatory in Review Board 9.0')
+        diff_settings = DiffSettings.create(request=request)
+
     if (diffset.commit_count > 0 and
         base_commit and
         tip_commit and
@@ -1376,9 +1415,9 @@ def get_diff_files(
                         base_filediff = requested_base_filediff
                     else:
                         raise ValueError(
-                            'Invalid base_filediff (ID %d) for filediff (ID '
-                            '%d)'
-                            % (requested_base_filediff.pk, filediff.pk))
+                            f'Invalid base_filediff (ID '
+                            f'{requested_base_filediff.pk}) for filediff (ID '
+                            f'{filediff.pk})')
                 elif base_commit:
                     base_filediff = filediff.get_base_filediff(
                         base_commit=base_commit,
@@ -1389,12 +1428,18 @@ def get_diff_files(
                     newfile = ancestors[0].is_new
                     orig_revision = get_revision_str(PRE_CREATION)
 
+        extra: DiffFileExtraContext = {}
+
+        if diff_settings.tab_size:
+            extra['tab_size'] = diff_settings.tab_size
+
         f: SerializedDiffFile = {
             'base_filediff': base_filediff,
             'binary': filediff.binary,
             'chunks_loaded': False,
             'copied': filediff.copied,
             'deleted': filediff.deleted,
+            'extra': extra,
             'filediff': filediff,
             'force_interdiff': force_interdiff,
             'index': len(files),
@@ -1604,13 +1649,13 @@ def get_file_from_filediff(
     """
     interdiffset = None
 
-    key = "_diff_files_%s_%s" % (filediff.diffset.id, filediff.id)
+    key = f'_diff_files_{filediff.diffset.pk}_{filediff.pk}'
 
     if base_filediff:
-        key += '_base%s' % base_filediff.id
+        key += f'_base{base_filediff.pk}'
 
     if interfilediff:
-        key += "_%s" % interfilediff.id
+        key += f'_{interfilediff.pk}'
         interdiffset = interfilediff.diffset
 
     if key in context:
@@ -1618,16 +1663,17 @@ def get_file_from_filediff(
     else:
         assert 'user' in context
 
-        request = context.get('request', None)
+        request = context.get('request')
         files = get_diff_files(
-            filediff.diffset,
-            filediff,
-            interdiffset,
+            diffset=filediff.diffset,
+            filediff=filediff,
+            interdiffset=interdiffset,
             interfilediff=interfilediff,
             base_filediff=base_filediff,
             request=request,
             base_commit=base_commit,
-            tip_commit=tip_commit)
+            tip_commit=tip_commit,
+            diff_settings=diff_settings)
 
         populate_diff_chunks(files=files,
                              request=request,
diff --git a/reviewboard/diffviewer/opcode_generator.py b/reviewboard/diffviewer/opcode_generator.py
index 0795745c2cd710a7e3a9b0ce925646df5a73f333..599053c6fada37767a5ca9a6423f138e1101ba41 100644
--- a/reviewboard/diffviewer/opcode_generator.py
+++ b/reviewboard/diffviewer/opcode_generator.py
@@ -1,8 +1,19 @@
+"""The diff opcode generator."""
+
+from __future__ import annotations
+
 import os
 import re
+from typing import Optional, TYPE_CHECKING
 
 from reviewboard.diffviewer.processors import (filter_interdiff_opcodes,
                                                post_process_filtered_equals)
+from reviewboard.diffviewer.settings import DiffSettings
+
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
+    from reviewboard.diffviewer.differ import Differ
 
 
 class MoveRange(object):
@@ -28,19 +39,56 @@ class MoveRange(object):
         return '<MoveRange(%d, %d, %r)>' % (self.start, self.end, self.groups)
 
 
-class DiffOpcodeGenerator(object):
+class DiffOpcodeGenerator:
+    """The diff opcode generator."""
+
     ALPHANUM_RE = re.compile(r'\w')
     WHITESPACE_RE = re.compile(r'\s')
 
     MOVE_PREFERRED_MIN_LINES = 2
     MOVE_MIN_LINE_LENGTH = 20
 
-    TAB_SIZE = 8
+    #: The default width for a tabstop.
+    TAB_SIZE = DiffSettings.DEFAULT_TAB_SIZE
 
-    def __init__(self, differ, diff=None, interdiff=None, request=None,
-                 **kwargs):
+    ######################
+    # Instance variables #
+    ######################
+
+    #: The raw contents for the diff.
+    diff: Optional[bytes]
+
+    #: The differ being used to generate the diff.
+    differ: Differ
+
+    #: The diff settings object.
+    #:
+    #: Version Added:
+    #:     7.0.4
+    diff_settings: DiffSettings
+
+    #: The raw contents of the interdiff range diff.
+    interdiff: Optional[bytes]
+
+    #: The HTTP request from the client.
+    request: Optional[HttpRequest]
+
+    def __init__(
+        self,
+        differ: Differ,
+        diff: Optional[bytes] = None,
+        interdiff: Optional[bytes] = None,
+        request: Optional[HttpRequest] = None,
+        *,
+        diff_settings: Optional[DiffSettings] = None,
+        **kwargs,
+    ) -> None:
         """Initialize the opcode generator.
 
+        Version Changed:
+            7.0.4:
+            Added the ``diff_settings`` parameter.
+
         Version Changed:
             3.0.18:
             Added the ``request`` and ``**kwargs`` parameters.
@@ -59,6 +107,13 @@ class DiffOpcodeGenerator(object):
             request (django.http.HttpRequest):
                 The HTTP request from the client.
 
+            diff_settings (reviewboard.diffviewer.settings.DiffSettings,
+                           optional):
+                The diff settings object.
+
+                Version Added:
+                    7.0.4
+
             **kwargs (dict):
                 Additional keyword arguments, for future expansion.
         """
@@ -67,6 +122,12 @@ class DiffOpcodeGenerator(object):
         self.interdiff = interdiff
         self.request = request
 
+        if diff_settings is None:
+            diff_settings = DiffSettings.create(request=request)
+
+        assert diff_settings.tab_size
+        self.diff_settings = diff_settings
+
     def __iter__(self):
         """Returns opcodes from the differ with extra metadata.
 
@@ -351,8 +412,11 @@ class DiffOpcodeGenerator(object):
         old_line_indent = old_line[:old_line_indent_len]
         new_line_indent = new_line[:new_line_indent_len]
 
-        norm_old_line_indent = old_line_indent.expandtabs(self.TAB_SIZE)
-        norm_new_line_indent = new_line_indent.expandtabs(self.TAB_SIZE)
+        tab_size = self.diff_settings.tab_size
+        assert tab_size
+
+        norm_old_line_indent = old_line_indent.expandtabs(tab_size)
+        norm_new_line_indent = new_line_indent.expandtabs(tab_size)
         norm_old_line_indent_len = len(norm_old_line_indent)
         norm_new_line_indent_len = len(norm_new_line_indent)
         norm_old_line_len = (norm_old_line_indent_len +
@@ -733,18 +797,42 @@ class DiffOpcodeGenerator(object):
 _generator = DiffOpcodeGenerator
 
 
-def get_diff_opcode_generator_class():
-    """Returns the DiffOpcodeGenerator class used for generating opcodes."""
+def get_diff_opcode_generator_class() -> type[DiffOpcodeGenerator]:
+    """Return the DiffOpcodeGenerator class used for generating opcodes.
+
+    Returns:
+        type:
+        The opcode generator class.
+    """
     return _generator
 
 
-def set_diff_opcode_generator_class(renderer):
-    """Sets the DiffOpcodeGenerator class used for generating opcodes."""
+def set_diff_opcode_generator_class(
+    renderer: type[DiffOpcodeGenerator],
+) -> None:
+    """Set the DiffOpcodeGenerator class used for generating opcodes.
+
+    Args:
+        renderer (type):
+            The opcode generator class.
+    """
     assert renderer
 
     globals()['_generator'] = renderer
 
 
-def get_diff_opcode_generator(*args, **kwargs):
-    """Returns a DiffOpcodeGenerator instance used for generating opcodes."""
+def get_diff_opcode_generator(*args, **kwargs) -> DiffOpcodeGenerator:
+    """Return a DiffOpcodeGenerator instance used for generating opcodes.
+
+    Args:
+        *args (tuple):
+            Positional arguments to pass to the opcode generator.
+
+        **kwargs (dict):
+            Keyword arguments to pass to the opcode generator.
+
+    Returns:
+        DiffOpcodeGenerator:
+        The new opcode generator.
+    """
     return _generator(*args, **kwargs)
diff --git a/reviewboard/diffviewer/settings.py b/reviewboard/diffviewer/settings.py
index 26167a84d02f5b63d2ca447165023b8ce16e5ebb..d27e95c810d44c9b3d64505b800d4b7df312f51b 100644
--- a/reviewboard/diffviewer/settings.py
+++ b/reviewboard/diffviewer/settings.py
@@ -9,15 +9,17 @@ from __future__ import annotations
 import json
 from dataclasses import dataclass, fields as dataclass_fields
 from hashlib import sha256
-from typing import Any, Dict, List, Optional, cast
+from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, cast
 
 from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
-from django.http import HttpRequest
 from django.utils.functional import cached_property
 from djblets.siteconfig.models import SiteConfiguration
 
-from reviewboard.site.models import LocalSite
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
+    from reviewboard.site.models import LocalSite
 
 
 @dataclass
@@ -33,6 +35,16 @@ class DiffSettings:
         5.0.2
     """
 
+    #: The default for tabstop widths.
+    #:
+    #: Version Added:
+    #:     7.0.4
+    DEFAULT_TAB_SIZE: ClassVar[int] = 8
+
+    ######################
+    # Instance variables #
+    ######################
+
     #: A mapping of code safety checker IDs to configurations.
     #:
     #: Type:
@@ -94,6 +106,12 @@ class DiffSettings:
     #:     int
     syntax_highlighting_threshold: int
 
+    #: The default tabstop width for diffs.
+    #:
+    #: Version Added:
+    #:     7.0.4
+    tab_size: int
+
     @classmethod
     def create(
         cls,
@@ -166,6 +184,12 @@ class DiffSettings:
 
             assert syntax_highlighting is not None
 
+        tab_size = cast(Optional[int],
+                        siteconfig.get('diffviewer_default_tab_size'))
+
+        if not tab_size:
+            tab_size = cls.DEFAULT_TAB_SIZE
+
         return cls(
             code_safety_configs=cast(
                 Dict,
@@ -189,7 +213,9 @@ class DiffSettings:
             syntax_highlighting=syntax_highlighting,
             syntax_highlighting_threshold=cast(
                 int,
-                siteconfig.get('diffviewer_syntax_highlighting_threshold')))
+                siteconfig.get('diffviewer_syntax_highlighting_threshold')),
+            tab_size=tab_size,
+        )
 
     @cached_property
     def state_hash(self) -> str:
diff --git a/reviewboard/diffviewer/tests/test_diff_settings.py b/reviewboard/diffviewer/tests/test_diff_settings.py
index 9b3ba114224c1f7fd140845153954209edd72dc4..33fb6f94fa117e6f38b311108807912d49251459 100644
--- a/reviewboard/diffviewer/tests/test_diff_settings.py
+++ b/reviewboard/diffviewer/tests/test_diff_settings.py
@@ -4,9 +4,15 @@ Version Added:
     5.0.2
 """
 
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from reviewboard.diffviewer.settings import DiffSettings
 from reviewboard.testing import TestCase
 
+if TYPE_CHECKING:
+    from djblets.util.typing import JSONDict
 
 class DiffSettingsTests(TestCase):
     """Unit tests for reviewboard.diffviewer.settings.DiffSettings."""
@@ -112,9 +118,31 @@ class DiffSettingsTests(TestCase):
 
         self.assertFalse(diff_settings.syntax_highlighting)
 
-    def test_state_hash(self):
+    def test_tab_size(self) -> None:
+        """Testing DiffSettings.tab_size normalization"""
+        with self.siteconfig_settings(
+            {'diffviewer_default_tab_size': None},
+        ):
+            diff_settings = DiffSettings.create()
+            self.assertEqual(diff_settings.tab_size,
+                             DiffSettings.DEFAULT_TAB_SIZE)
+
+        with self.siteconfig_settings(
+            {'diffviewer_default_tab_size': 0},
+        ):
+            diff_settings = DiffSettings.create()
+            self.assertEqual(diff_settings.tab_size,
+                             DiffSettings.DEFAULT_TAB_SIZE)
+
+        with self.siteconfig_settings(
+            {'diffviewer_default_tab_size': 4},
+        ):
+            diff_settings = DiffSettings.create()
+            self.assertEqual(diff_settings.tab_size, 4)
+
+    def test_state_hash(self) -> None:
         """Testing DiffSettings.state_hash"""
-        siteconfig_settings = {
+        siteconfig_settings: JSONDict = {
             'code_safety_checkers': {
                 'trojan_code': {
                     'enable_confusables': True,
@@ -124,6 +152,7 @@ class DiffSettingsTests(TestCase):
             'diffviewer_custom_pygments_lexers': {
                 '.foo': 'SomeLexer',
             },
+            'diffviewer_default_tab_size': 4,
             'diffviewer_include_space_patterns': ['*.a', '*.b'],
             'diffviewer_paginate_by': 20,
             'diffviewer_paginate_orphans': 5,
@@ -136,4 +165,4 @@ class DiffSettingsTests(TestCase):
 
         self.assertEqual(
             diff_settings.state_hash,
-            '178099a49bf920fd46866042e9013d90d8ad31edf986325cdd641d2e1de4e01f')
+            '2941f28d668cbe4ab38659f9b7369f649058a3d70393897dc5f8108b764e271b')
diff --git a/reviewboard/diffviewer/tests/test_diffutils.py b/reviewboard/diffviewer/tests/test_diffutils.py
index 2a865cfe120ded46950a64ac75bb85c96ac5fa7f..e5d5953f225be1f5f18046479ab104ba756fc39a 100644
--- a/reviewboard/diffviewer/tests/test_diffutils.py
+++ b/reviewboard/diffviewer/tests/test_diffutils.py
@@ -591,7 +591,9 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
                              dest_file='foo3.txt', status=FileDiff.MODIFIED,
                              diff=one_to_three)
 
-        diff_files = get_diff_files(diffset=diffset, interdiffset=interdiffset)
+        diff_files = get_diff_files(diffset=diffset,
+                                    interdiffset=interdiffset,
+                                    diff_settings=DiffSettings.create())
         two_to_three = diff_files[0]
 
         self.assertEqual(two_to_three['orig_filename'], 'foo2.txt')
@@ -688,7 +690,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
             diff=b'interdiff3')
 
         diff_files = get_diff_files(diffset=diffset,
-                                    interdiffset=interdiffset)
+                                    interdiffset=interdiffset,
+                                    diff_settings=DiffSettings.create())
         self.assertEqual(len(diff_files), 6)
 
         diff_file = diff_files[0]
@@ -799,7 +802,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         diff_files = get_diff_files(diffset=diffset,
                                     interdiffset=interdiffset,
-                                    filediff=filediff)
+                                    filediff=filediff,
+                                    diff_settings=DiffSettings.create())
         self.assertEqual(len(diff_files), 1)
 
         diff_file = diff_files[0]
@@ -860,7 +864,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         diff_files = get_diff_files(diffset=diffset,
                                     interdiffset=interdiffset,
                                     filediff=filediff,
-                                    interfilediff=interfilediff)
+                                    interfilediff=interfilediff,
+                                    diff_settings=DiffSettings.create())
         self.assertEqual(len(diff_files), 1)
 
         diff_file = diff_files[0]
@@ -881,7 +886,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
                                                     create_with_history=True)
         review_request.diffset_history.diffsets.add(self.diffset)
 
-        result = get_diff_files(diffset=self.diffset)
+        result = get_diff_files(diffset=self.diffset,
+                                diff_settings=DiffSettings.create())
 
         self.assertEqual(len(result), len(self.diffset.cumulative_files))
 
@@ -911,7 +917,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         #
         # 1. Select all FileDiffs for a DiffSet.
         with self.assertNumQueries(1):
-            get_diff_files(diffset=self.diffset)
+            get_diff_files(diffset=self.diffset,
+                           diff_settings=DiffSettings.create())
 
     def test_get_diff_files_history_query_count_ancestors_precomputed(self):
         """Testing get_diff_files query count for a whole diffset with history
@@ -930,7 +937,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         #
         # 1. Select all FileDiffs for a DiffSet.
         with self.assertNumQueries(1):
-            get_diff_files(diffset=self.diffset)
+            get_diff_files(diffset=self.diffset,
+                           diff_settings=DiffSettings.create())
 
     def test_get_diff_files_query_count_filediff(self):
         """Testing get_diff_files for a single FileDiff with history"""
@@ -947,7 +955,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         with self.assertNumQueries(4):
             files = get_diff_files(diffset=self.diffset,
-                                   filediff=filediff)
+                                   filediff=filediff,
+                                   diff_settings=DiffSettings.create())
 
         self.assertEqual(len(files), 1)
         f = files[0]
@@ -976,7 +985,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         with self.assertNumQueries(1):
             files = get_diff_files(diffset=self.diffset,
-                                   filediff=filediff)
+                                   filediff=filediff,
+                                   diff_settings=DiffSettings.create())
 
         self.assertEqual(len(files), 1)
         f = files[0]
@@ -1013,7 +1023,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         # 9. Update extra_data on FileDiff id=9.
         with self.assertNumQueries(9):
             files = get_diff_files(diffset=self.diffset,
-                                   base_commit=diff_commit)
+                                   base_commit=diff_commit,
+                                   diff_settings=DiffSettings.create())
 
         expected_results = self._get_filediff_base_mapping_from_details(
             self.get_filediffs_by_details(),
@@ -1050,7 +1061,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         review_request.diffset_history.diffsets.add(self.diffset)
 
         files = get_diff_files(diffset=self.diffset,
-                               base_commit=DiffCommit.objects.get(pk=4))
+                               base_commit=DiffCommit.objects.get(pk=4),
+                               diff_settings=DiffSettings.create())
 
         self.assertEqual(files, [])
 
@@ -1083,7 +1095,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         # 9. Update extra_data on FileDiff id=9.
         with self.assertNumQueries(9):
             files = get_diff_files(diffset=self.diffset,
-                                   tip_commit=tip_commit)
+                                   tip_commit=tip_commit,
+                                   diff_settings=DiffSettings.create())
 
         expected_results = self._get_filediff_base_mapping_from_details(
             self.get_filediffs_by_details(),
@@ -1133,7 +1146,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         # 1. Select all FileDiffs for a DiffSet.
         with self.assertNumQueries(1):
             files = get_diff_files(diffset=self.diffset,
-                                   tip_commit=tip_commit)
+                                   tip_commit=tip_commit,
+                                   diff_settings=DiffSettings.create())
 
         expected_results = self._get_filediff_base_mapping_from_details(
             self.get_filediffs_by_details(),
@@ -1194,7 +1208,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         with self.assertNumQueries(8):
             files = get_diff_files(diffset=self.diffset,
                                    base_commit=base_commit,
-                                   tip_commit=tip_commit)
+                                   tip_commit=tip_commit,
+                                   diff_settings=DiffSettings.create())
 
         expected_results = self._get_filediff_base_mapping_from_details(
             self.get_filediffs_by_details(),
@@ -1243,7 +1258,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         with self.assertNumQueries(1):
             files = get_diff_files(diffset=self.diffset,
                                    base_commit=base_commit,
-                                   tip_commit=tip_commit)
+                                   tip_commit=tip_commit,
+                                   diff_settings=DiffSettings.create())
 
         expected_results = self._get_filediff_base_mapping_from_details(
             self.get_filediffs_by_details(),
@@ -1280,7 +1296,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         files = get_diff_files(diffset=self.diffset,
                                base_commit=None,
-                               tip_commit=commits[0])
+                               tip_commit=commits[0],
+                               diff_settings=DiffSettings.create())
 
         # File "foo" was added in the first commit in the series.
 
@@ -1293,7 +1310,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         files = get_diff_files(diffset=self.diffset,
                                base_commit=commits[0],
-                               tip_commit=commits[1])
+                               tip_commit=commits[1],
+                               diff_settings=DiffSettings.create())
 
         self.assertEqual(files[2]['modified_filename'], 'foo')
         self.assertFalse(files[1]['filediff'].is_new)
@@ -1359,7 +1377,9 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         self.spy_on(tool_class.normalize_path_for_display,
                     owner=tool_class)
 
-        get_diff_files(diffset=diffset, filediff=filediff)
+        get_diff_files(diffset=diffset,
+                       filediff=filediff,
+                       diff_settings=DiffSettings.create())
 
         self.assertSpyCalledWith(tool_class.normalize_path_for_display,
                                  'foo.txt', extra_data={'test': True})
@@ -1375,7 +1395,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         self.create_filediff(diffset=diffset, source_file='foo.txt',
                              dest_file='foo2.txt', status=FileDiff.MODIFIED)
 
-        diff_files = get_diff_files(diffset=diffset)
+        diff_files = get_diff_files(diffset=diffset,
+                                    diff_settings=DiffSettings.create())
         self.assertFalse(diff_files[0]['public'])
 
         draft = review_request.get_draft()
@@ -1383,7 +1404,8 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
         draft.publish()
 
         diffset.refresh_from_db()
-        diff_files = get_diff_files(diffset=diffset)
+        diff_files = get_diff_files(diffset=diffset,
+                                    diff_settings=DiffSettings.create())
         self.assertTrue(diff_files[0]['public'])
 
     def test_get_diff_files_public_state_with_interdiff(self):
@@ -1408,7 +1430,9 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
                              status=FileDiff.MODIFIED,
                              diff=b'interdiff2')
 
-        diff_files = get_diff_files(diffset=diffset, interdiffset=interdiffset)
+        diff_files = get_diff_files(diffset=diffset,
+                                    interdiffset=interdiffset,
+                                    diff_settings=DiffSettings.create())
         self.assertFalse(diff_files[0]['public'])
 
         draft = review_request.get_draft()
@@ -1417,7 +1441,9 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         diffset.refresh_from_db()
         interdiffset.refresh_from_db()
-        diff_files = get_diff_files(diffset=diffset, interdiffset=interdiffset)
+        diff_files = get_diff_files(diffset=diffset,
+                                    interdiffset=interdiffset,
+                                    diff_settings=DiffSettings.create())
         self.assertTrue(diff_files[0]['public'])
 
     def test_get_diff_files_added_in_ancestor(self) -> None:
@@ -1434,8 +1460,10 @@ class GetDiffFilesTests(BaseFileDiffAncestorTests):
 
         # file 'foo' added in commit pk=3, and then renamed/edited to 'qux' in
         # pk=4.
-        files = get_diff_files(diffset=self.diffset, tip_commit=tip_commit,
-                               filename_patterns=['qux'])
+        files = get_diff_files(diffset=self.diffset,
+                               tip_commit=tip_commit,
+                               filename_patterns=['qux'],
+                               diff_settings=DiffSettings.create())
 
         self.assertEqual(len(files), 1)
 
diff --git a/reviewboard/diffviewer/views.py b/reviewboard/diffviewer/views.py
index fc10399ce270c3e9f3ef8f5e9ee72ae6bda88658..f1578f1cafebe1fb3296fbe03865b1c1eebd5ea6 100644
--- a/reviewboard/diffviewer/views.py
+++ b/reviewboard/diffviewer/views.py
@@ -7,7 +7,7 @@ import os
 import re
 import traceback
 from io import BytesIO
-from typing import Any, Optional, TYPE_CHECKING
+from typing import Any, Mapping, Optional, TYPE_CHECKING, Union
 from zipfile import ZipFile
 
 from django.conf import settings
@@ -374,19 +374,19 @@ class DiffViewerView(TemplateView):
         except KeyError:
             filename_patterns = []
 
-        files = get_diff_files(diffset=diffset,
-                               interdiffset=interdiffset,
-                               request=self.request,
-                               filename_patterns=filename_patterns,
-                               base_commit=base_commit,
-                               tip_commit=tip_commit)
-
-        # Break the list of files into pages
-        siteconfig = SiteConfiguration.objects.get_current()
+        diff_settings = DiffSettings.create(request=self.request)
+        files = get_diff_files(
+            diffset=diffset,
+            interdiffset=interdiffset,
+            request=self.request,
+            filename_patterns=filename_patterns,
+            base_commit=base_commit,
+            tip_commit=tip_commit,
+            diff_settings=diff_settings)
 
         paginator = Paginator(files,
-                              siteconfig.get('diffviewer_paginate_by'),
-                              siteconfig.get('diffviewer_paginate_orphans'))
+                              diff_settings.paginate_by,
+                              diff_settings.paginate_orphans)
 
         page_num = int(self.request.GET.get('page', 1))
 
@@ -395,10 +395,8 @@ class DiffViewerView(TemplateView):
 
             for i, f in enumerate(files):
                 if f['filediff'].pk == file_id:
-                    page_num = i // paginator.per_page + 1
-
-                    if page_num > paginator.num_pages:
-                        page_num = paginator.num_pages
+                    page_num = min(i // paginator.per_page + 1,
+                                   paginator.num_pages)
 
                     break
 
@@ -715,9 +713,15 @@ class DiffFragmentView(View):
                interfilediff_id,
                settings.TEMPLATE_SERIAL))
 
-    def process_diffset_info(self, diffset_or_id, filediff_id,
-                             interfilediff_id=None, interdiffset_or_id=None,
-                             base_filediff_id=None, **kwargs):
+    def process_diffset_info(
+        self,
+        diffset_or_id: Union[DiffSet, int],
+        filediff_id: int,
+        interfilediff_id: Optional[int] = None,
+        interdiffset_or_id: Optional[Union[DiffSet, int]] = None,
+        base_filediff_id: Optional[int] = None,
+        **kwargs,
+    ) -> Mapping[str, Any]:
         """Process and return information on the desired diff.
 
         The diff IDs and other data passed to the view can be processed and
@@ -726,6 +730,33 @@ class DiffFragmentView(View):
 
         A subclass may instead return a HttpResponse to indicate an error
         with the DiffSets.
+
+        Args:
+            diffset_or_id (reviewboard.diffviewer.models.diffset.DiffSet or
+                           int):
+                The DiffSet object, or the ID of the diffset.
+
+            filediff_id (int):
+                The ID of the FileDiff.
+
+            interfilediff_id (int, optional):
+                The ID of the FileDiff for rendering an interdiff, if present.
+
+            interdiffset_or_id (reviewboard.diffviewer.models.diffset.DiffSet
+                                or int):
+                The diffset object, or the ID of the diffset.
+
+            base_filediff_id (int):
+                The ID of the base FileDiff to use, if present. This may only
+                be provided if ``filediff_id`` is provided and
+                ``interfilediff_id`` is not.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments, for future expansion.
+
+        Returns:
+            dict:
+            Information about the diff.
         """
         # Depending on whether we're invoked from a URL or from a wrapper
         # with precomputed diffsets, we may be working with either IDs or
@@ -788,12 +819,19 @@ class DiffFragmentView(View):
                     % (base_filediff_id, filediff_id)
                 ))
 
+        assert diffset is not None
+
         # Store this so we don't end up causing an SQL query later when looking
         # this up.
         filediff.diffset = diffset
 
         diff_file = self._get_requested_diff_file(
-            diffset, filediff, interdiffset, interfilediff, base_filediff)
+            diffset=diffset,
+            filediff=filediff,
+            interdiffset=interdiffset,
+            interfilediff=interfilediff,
+            base_filediff=base_filediff,
+            diff_settings=DiffSettings.create(request=self.request))
 
         if not diff_file:
             raise UserVisibleError(
@@ -875,9 +913,17 @@ class DiffFragmentView(View):
             'show_deleted': show_deleted,
         }
 
-    def _get_requested_diff_file(self, diffset, filediff, interdiffset,
-                                 interfilediff, base_filediff):
-        """Fetches information on the requested diff.
+    def _get_requested_diff_file(
+        self,
+        *,
+        diffset: DiffSet,
+        filediff: Optional[FileDiff],
+        interdiffset: Optional[DiffSet],
+        interfilediff: Optional[FileDiff],
+        base_filediff: Optional[FileDiff],
+        diff_settings: Optional[DiffSettings],
+    ) -> Optional[SerializedDiffFile]:
+        """Fetch information on the requested diff.
 
         This will look up information on the diff that's to be rendered
         and return it, if found. It may also augment it with additional
@@ -885,13 +931,59 @@ class DiffFragmentView(View):
 
         The file will not contain chunk information. That must be specifically
         populated later.
+
+        Version Changed:
+            7.0.4:
+            * Made arguments keyword-only.
+            * Added the ``diff_settings`` argument.
+
+        Args:
+            diffset (reviewboard.diffviewer.models.diffset.DiffSet):
+                The diffset containing the files to return.
+
+            filediff (reviewboard.diffviewer.models.filediff.FileDiff,
+                      optional):
+                A specific file in the diff to return information for.
+
+            interdiffset (reviewboard.diffviewer.models.diffset.DiffSet,
+                          optional):
+                A second diffset used for an interdiff range.
+
+            interfilediff (reviewboard.diffviewer.models.filediff.FileDiff,
+                           optional):
+                A second specific file in ``interdiffset`` used to return
+                information for. This should be provided if ``filediff`` and
+                ``interdiffset`` are both provided. If it's ``None`` in this
+                case, then the diff will be shown as reverted for this file.
+
+                This may not be provided if ``base_filediff`` is provided.
+
+            base_filediff (reviewbaord.diffviewer.models.filediff.FileDiff,
+                           optional):
+                The base FileDiff to use.
+
+                This may only be provided if ``filediff`` is provided and
+                ``interfilediff`` is not.
+
+            diff_settings (reviewboard.diffviewer.settings.DiffSettings,
+                           optional):
+                The diff settings object. This will become mandatory in Review
+                Board 9.0.
+
+                Version Added:
+                    7.0.4
+
+        Returns:
+            SerializedDiffFile:
+            The serialized file information.
         """
         files = get_diff_files(diffset=diffset,
                                interdiffset=interdiffset,
                                filediff=filediff,
                                interfilediff=interfilediff,
                                base_filediff=base_filediff,
-                               request=self.request)
+                               request=self.request,
+                               diff_settings=diff_settings)
 
         if files:
             diff_file = files[0]
diff --git a/reviewboard/reviews/views/diffviewer.py b/reviewboard/reviews/views/diffviewer.py
index 27fd6d14f22dfccd949514df73ebe43b80660c09..a9fa34827e28c8d48fc17e6be6b9d1893f2896ab 100644
--- a/reviewboard/reviews/views/diffviewer.py
+++ b/reviewboard/reviews/views/diffviewer.py
@@ -13,6 +13,7 @@ from typing_extensions import NotRequired, TypeAlias, TypedDict
 from reviewboard.accounts.mixins import UserProfileRequiredViewMixin
 from reviewboard.attachments.models import get_latest_file_attachments
 from reviewboard.diffviewer.commit_utils import get_base_and_tip_commits
+from reviewboard.diffviewer.diffutils import DiffFileExtraContext
 from reviewboard.diffviewer.models import DiffCommit, DiffSet, FileDiff
 from reviewboard.diffviewer.views import (DiffViewerContext,
                                           DiffViewerView,
@@ -23,7 +24,6 @@ from reviewboard.reviews.context import (ReviewRequestContext,
 from reviewboard.reviews.ui.diff import DiffReviewUI
 from reviewboard.reviews.views.mixins import ReviewRequestViewMixin
 from reviewboard.reviews.models import (Review,
-                                        ReviewRequest,
                                         ReviewRequestDraft)
 
 if TYPE_CHECKING:
@@ -207,6 +207,12 @@ class SerializedReviewsDiffFile(TypedDict):
     #: Whether the file was deleted in the change.
     deleted: bool
 
+    #: Extra information about the diff file.
+    #:
+    #: Version Added:
+    #:     7.0.4
+    extra: DiffFileExtraContext
+
     #: The ID of the FileDiff.
     id: int
 
@@ -585,6 +591,7 @@ class ReviewsDiffViewerView(ReviewRequestViewMixin,
                 'base_filediff_id': base_filediff_id,
                 'binary': f['binary'],
                 'deleted': f['deleted'],
+                'extra': f['extra'],
                 'id': filediff.pk,
                 'index': f['index'],
                 'filediff': {
diff --git a/reviewboard/static/rb/js/reviews/models/diffFileModel.ts b/reviewboard/static/rb/js/reviews/models/diffFileModel.ts
index 23438165484eb060c9716269ce0a01637c4e6326..a4f1e4c61b1863e225bae8c7918d64c5b409ac9d 100644
--- a/reviewboard/static/rb/js/reviews/models/diffFileModel.ts
+++ b/reviewboard/static/rb/js/reviews/models/diffFileModel.ts
@@ -32,7 +32,19 @@ interface SerializedFileDiff {
 
 
 /** The set of serialized comment blocks in the diff. */
-type SerializedDiffCommentBlocks = { [key: string]: SerializedDiffComment };
+type SerializedDiffCommentBlocks = Record<string, SerializedDiffComment>;
+
+
+/**
+ * Extra context for rendering a diff file.
+ *
+ * Version Added:
+ *     7.0.4
+ */
+interface DiffFileExtraContext {
+    /* The tabstop width for a given file. */
+    tab_size: number | undefined;
+}
 
 
 /**
@@ -57,6 +69,9 @@ export interface DiffFileAttrs extends ModelAttributes {
     /** Whether or not the file was deleted. */
     deleted: boolean;
 
+    /** Extra context for rendering a diff file. */
+    extra: DiffFileExtraContext | null;
+
     /** Information about the filediff. */
     filediff: SerializedFileDiff | null;
 
@@ -134,6 +149,7 @@ export interface DiffFileResourceData {
     base_filediff_id: number;
     binary: boolean;
     deleted: boolean;
+    extra: DiffFileExtraContext;
     filediff: SerializedFileDiff;
     force_interdiff: boolean;
     id: number;
@@ -159,6 +175,7 @@ export class DiffFile extends BaseModel<DiffFileAttrs> {
         baseFileDiffID: null,
         binary: false,
         deleted: false,
+        extra: null,
         filediff: null,
         forceInterdiff: null,
         forceInterdiffRevision: null,
@@ -191,6 +208,7 @@ export class DiffFile extends BaseModel<DiffFileAttrs> {
             baseFileDiffID: rsp.base_filediff_id,
             binary: rsp.binary,
             deleted: rsp.deleted,
+            extra: rsp.extra,
             filediff: rsp.filediff,
             forceInterdiff: rsp.force_interdiff,
             forceInterdiffRevision: rsp.interdiff_revision,
diff --git a/reviewboard/static/rb/js/reviews/views/diffReviewableView.ts b/reviewboard/static/rb/js/reviews/views/diffReviewableView.ts
index 7887e06a5fe2ff295380efc253da38d063f38b3f..96322a463f14529c020871f8da6ddd44eb0b24d6 100644
--- a/reviewboard/static/rb/js/reviews/views/diffReviewableView.ts
+++ b/reviewboard/static/rb/js/reviews/views/diffReviewableView.ts
@@ -162,6 +162,13 @@ export class DiffReviewableView extends AbstractReviewableView<
             }
         });
 
+        const file = this.model.get('file');
+        const extra = file.get('extra');
+
+        if (extra?.tab_size) {
+            this.$el.css('tab-size', `${extra.tab_size}`);
+        }
+
         this._precalculateContentWidths();
         this._updateColumnSizes();
     }
diff --git a/reviewboard/webapi/resources/filediff.py b/reviewboard/webapi/resources/filediff.py
index fcd8cdeb74e12f83da6eff9f59ed21f6d234a62f..374951039fd6ac9b5b29a2b9a648ee4cf3de66f4 100644
--- a/reviewboard/webapi/resources/filediff.py
+++ b/reviewboard/webapi/resources/filediff.py
@@ -563,9 +563,11 @@ class FileDiffResource(WebAPIResource):
             request=request,
             syntax_highlighting=request.GET.get('syntax-highlighting', False))
 
-        files = get_diff_files(diffset=filediff.diffset,
-                               filediff=filediff,
-                               request=request)
+        files = get_diff_files(
+            diffset=filediff.diffset,
+            filediff=filediff,
+            request=request,
+            diff_settings=diff_settings)
         populate_diff_chunks(files=files,
                              request=request,
                              diff_settings=diff_settings)
